diff --git a/README.md b/README.md index f4af58f..bdc06d1 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,33 @@ class Role extends Model } ``` +### Refreshing Data on Each Request (Laravel Octane) + +When running under [Laravel Octane](https://laravel.com/docs/octane), the application process is kept alive between requests. By default, Sushi configures its SQLite connection once per process and reuses it for subsequent requests — which is usually desirable. + +If you need `getRows()` to be called again on every new request (for example, when your rows are sourced from a dynamic, per-request data source), you can override `shouldRefreshDataOnEachRequest()`: + +```php +class LiveSettings extends Model +{ + use \Sushi\Sushi; + + public function getRows(): array + { + return cache()->get('live-settings', []); + } + + protected static function shouldRefreshDataOnEachRequest(): bool + { + return true; + } +} +``` + +With this enabled, Sushi will detect when a new HTTP request starts (via the Laravel request object) and automatically re-run `getRows()`, rebuilding the in-memory SQLite table for that request. + +> Note: This has no effect outside of Octane (i.e. traditional FPM deployments), since each request already starts a fresh process. + ### Caching ->getRows() If you choose to use your own ->getRows() method, the rows will NOT be cached between requests by default. diff --git a/src/Sushi.php b/src/Sushi.php index 097394c..82d6e11 100644 --- a/src/Sushi.php +++ b/src/Sushi.php @@ -11,6 +11,7 @@ trait Sushi { protected static $sushiConnection; + protected static $sushiOctaneListenerRegistered = false; public function getRows() { @@ -32,8 +33,17 @@ protected function sushiShouldCache() return property_exists(static::class, 'rows'); } + protected static function shouldRefreshDataOnEachRequest(): bool + { + return false; + } + public static function resolveConnection($connection = null) { + if (static::$sushiConnection === null && static::shouldRefreshDataOnEachRequest()) { + static::configureSushiConnection(); + } + return static::$sushiConnection; } @@ -68,12 +78,45 @@ public static function bootSushi() if (method_exists(static::class, 'whenBooted')) { static::whenBooted(function () { static::configureSushiConnection(); + static::registerOctaneRefreshListener(); }); } else { static::configureSushiConnection(); + static::registerOctaneRefreshListener(); } } + protected static function registerOctaneRefreshListener(): void + { + if (! static::shouldRefreshDataOnEachRequest()) { + return; + } + + if (static::$sushiOctaneListenerRegistered) { + return; + } + + if (! class_exists('Laravel\Octane\Events\RequestReceived')) { + return; + } + + $class = static::class; + + app('events')->listen( + \Laravel\Octane\Events\RequestReceived::class, + static function () use ($class) { + $class::clearSushiConnection(); + } + ); + + static::$sushiOctaneListenerRegistered = true; + } + + public static function clearSushiConnection(): void + { + static::$sushiConnection = null; + } + protected static function configureSushiConnection() { $instance = new static; diff --git a/tests/SushiTest.php b/tests/SushiTest.php index 3ee6d9b..ebfc02a 100644 --- a/tests/SushiTest.php +++ b/tests/SushiTest.php @@ -27,6 +27,7 @@ public function setUp(): void Foo::resetStatics(); Bar::resetStatics(); + Bar::$hasBeenAccessedBefore = false; File::cleanDirectory($this->cachePath); } @@ -193,6 +194,34 @@ function test_sushi_models_can_relate_to_models_in_regular_sqlite_databases() $this->assertCount(2, $californiaMaki->ingredients); } + function test_model_with_refresh_reruns_get_rows_after_connection_is_cleared() + { + OctaneRefreshModel::resetStatics(); + + $firstCount = OctaneRefreshModel::count(); // getRows runs once → 1 row + + // Simulate what Octane's RequestReceived event does + OctaneRefreshModel::clearSushiConnection(); + + $secondCount = OctaneRefreshModel::count(); // getRows runs again → 2 rows + + $this->assertGreaterThan($firstCount, $secondCount); + $this->assertEquals(2, OctaneRefreshModel::$callCount); + } + + function test_model_without_refresh_does_not_register_octane_listener() + { + // Bar has shouldRefreshDataOnEachRequest() = false (default) + Bar::$hasBeenAccessedBefore = false; + Bar::resetStatics(); + + Bar::count(); // triggers boot → registerOctaneRefreshListener() returns early + + $prop = (new \ReflectionClass(Bar::class))->getProperty('sushiOctaneListenerRegistered'); + $prop->setAccessible(true); + $this->assertFalse($prop->getValue()); + } + protected function usesSqliteConnection($app) { file_put_contents(__DIR__ . '/database/database.sqlite', ''); @@ -219,6 +248,7 @@ class Foo extends Model public static function resetStatics() { static::setSushiConnection(null); + static::$sushiOctaneListenerRegistered = false; static::clearBootedModels(); } @@ -310,6 +340,7 @@ public function getRows() public static function resetStatics() { static::setSushiConnection(null); + static::$sushiOctaneListenerRegistered = false; static::clearBootedModels(); } @@ -361,3 +392,28 @@ public function maki() return $this->belongsTo(Maki::class); } } + +class OctaneRefreshModel extends Model +{ + use \Sushi\Sushi; + + public static $callCount = 0; + + public function getRows(): array + { + return array_fill(0, ++static::$callCount, ['name' => 'row']); + } + + protected static function shouldRefreshDataOnEachRequest(): bool + { + return true; + } + + public static function resetStatics(): void + { + static::$sushiConnection = null; + static::$sushiOctaneListenerRegistered = false; + static::$callCount = 0; + static::clearBootedModels(); + } +}