From 21b950e0bfdf90f5c971dd576c6e1bb2dbe44bf0 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 17 Nov 2024 23:16:42 +0000 Subject: [PATCH] feat: Introduce deferrable service overrides --- src/Concerns/HandlesServiceOverrides.php | 380 +++++++++++++++++++ src/Contracts/DeferrableServiceOverride.php | 20 + src/Listeners/CleanupServiceOverrides.php | 4 +- src/Listeners/SetupServiceOverrides.php | 4 +- src/Overrides/CacheOverride.php | 14 +- src/Overrides/CookieOverride.php | 13 +- src/Overrides/SessionOverride.php | 13 +- src/Overrides/StorageOverride.php | 13 +- src/Sprout.php | 49 +-- src/SproutServiceProvider.php | 15 +- tests/Http/Resolvers/SessionResolverTest.php | 2 +- tests/Overrides/CookieOverrideTest.php | 2 +- tests/Overrides/StorageOverrideTest.php | 28 +- 13 files changed, 487 insertions(+), 70 deletions(-) create mode 100644 src/Concerns/HandlesServiceOverrides.php create mode 100644 src/Contracts/DeferrableServiceOverride.php diff --git a/src/Concerns/HandlesServiceOverrides.php b/src/Concerns/HandlesServiceOverrides.php new file mode 100644 index 0000000..0f31be0 --- /dev/null +++ b/src/Concerns/HandlesServiceOverrides.php @@ -0,0 +1,380 @@ +> + */ + private array $registeredOverrides = []; + + /** + * @var array, \Sprout\Contracts\ServiceOverride> + */ + private array $overrides = []; + + /** + * @var array, string|class-string> + */ + private array $deferredOverrides = []; + + /** + * @var array, BootableServiceOverride> + */ + private array $bootableOverrides = []; + + /** + * @var array, bool> + */ + private array $bootedOverrides = []; + + /** + * @var array> + */ + private array $setupOverrides = []; + + /** + * @var bool + */ + private bool $hasBooted = false; + + /** + * Register a service override + * + * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * + * @return static + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function registerOverride(string $overrideClass): static + { + if (! is_subclass_of($overrideClass, ServiceOverride::class)) { + throw new InvalidArgumentException('Provided service override [' . $overrideClass . '] does not implement ' . ServiceOverride::class); + } + + // Flag the service override as being registered + $this->registeredOverrides[] = $overrideClass; + + if (is_subclass_of($overrideClass, DeferrableServiceOverride::class)) { + $this->registerDeferrableOverride($overrideClass); + } else { + $this->processOverride($overrideClass); + } + + return $this; + } + + /** + * Process the registration of a service override + * + * This method is an abstraction of the service override registration + * processing, which exists entirely to make deferrable overrides easier. + * + * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * + * @return static + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + protected function processOverride(string $overrideClass): static + { + // Create a new instance of the override + $override = $this->app->make($overrideClass); + + // Register the instance + $this->overrides[$overrideClass] = $override; + + // The override is bootable + if ($override instanceof BootableServiceOverride) { + // So register it as one + $this->bootableOverrides[$overrideClass] = $override; + $this->bootedOverrides[$overrideClass] = false; + + // If the boot phase has already happened, we'll boot it now + if ($this->haveOverridesBooted()) { + $this->bootOverride($overrideClass); + } + } + + return $this; + } + + /** + * Register a deferrable service override + * + * @param class-string<\Sprout\Contracts\DeferrableServiceOverride> $overrideClass + * + * @return static + */ + protected function registerDeferrableOverride(string $overrideClass): static + { + // Register the deferred override and its service + $this->deferredOverrides[$overrideClass] = $overrideClass::service(); + + $this->app->afterResolving($overrideClass::service(), function () use ($overrideClass) { + $this->processOverride($overrideClass); + + // Get the current tenancy + $tenancy = $this->getCurrentTenancy(); + + // If there's a current tenancy WITH a tenant, we can set up the + // override + if ($tenancy !== null && $tenancy->check()) { + $this->setupOverride($overrideClass, $tenancy, $tenancy->tenant()); + } + }); + + return $this; + } + + /** + * Check if a service override is bootable + * + * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * + * @return bool + */ + public function isBootableOverride(string $overrideClass): bool + { + return isset($this->bootableOverrides[$overrideClass]); + } + + /** + * Check if a service override has been booted + * + * This method returns true if the service override has been booted, or + * false if either it hasn't, or it isn't bootable. + * + * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * + * @return bool + */ + public function hasBootedOverride(string $overrideClass): bool + { + return $this->bootedOverrides[$overrideClass] ?? false; + } + + /** + * Check if the boot phase has already happened + * + * @return bool + */ + public function haveOverridesBooted(): bool + { + return $this->hasBooted; + } + + /** + * Boot all bootable overrides + * + * @return void + */ + public function bootOverrides(): void + { + // If the boot phase for the override has already happened, skip it + if ($this->haveOverridesBooted()) { + return; + } + + foreach ($this->bootableOverrides as $overrideClass => $override) { + // It's possible this is being called a second time, so we don't + // want to do it again + if (! $this->hasBootedOverride($overrideClass)) { + // Boot the override + $this->bootOverride($overrideClass); + } + } + + // Mark the override boot phase as having completed + $this->hasBooted = true; + } + + /** + * Boot a service override + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param class-string<\Sprout\Contracts\BootableServiceOverride> $overrideClass + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + protected function bootOverride(string $overrideClass): void + { + $this->overrides[$overrideClass]->boot($this->app, $this); + $this->bootedOverrides[$overrideClass] = true; + } + + /** + * Check if a service override has been set up + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * + * @return bool + */ + public function hasSetupOverride(Tenancy $tenancy, string $overrideClass): bool + { + return $this->setupOverrides[$tenancy->getName()][$overrideClass] ?? false; + } + + /** + * Set-up all available service overrides + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + public function setupOverrides(Tenancy $tenancy, Tenant $tenant): void + { + foreach ($this->overrides as $overrideClass => $override) { + if (! $this->hasSetupOverride($tenancy, $overrideClass)) { + $this->setupOverride($overrideClass, $tenancy, $tenant); + } + } + } + + /** + * Set up a service override + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + protected function setupOverride(string $overrideClass, Tenancy $tenancy, Tenant $tenant): void + { + $this->overrides[$overrideClass]->setup($tenancy, $tenant); + $this->setupOverrides[$tenancy->getName()][$overrideClass] = true; + } + + /** + * Clean-up all service overrides + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + public function cleanupOverrides(Tenancy $tenancy, Tenant $tenant): void + { + $overrides = $this->setupOverrides[$tenancy->getName()] ?? []; + + foreach ($overrides as $overrideClass => $status) { + if ($status === true) { + $this->cleanupOverride($overrideClass, $tenancy, $tenant); + } + } + } + + /** + * Clean-up a service override + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + protected function cleanupOverride(string $overrideClass, Tenancy $tenancy, Tenant $tenant): void + { + $this->overrides[$overrideClass]->cleanup($tenancy, $tenant); + unset($this->setupOverrides[$tenancy->getName()][$overrideClass]); + } + + /** + * Get all service overrides + * + * @return array, \Sprout\Contracts\ServiceOverride> + */ + public function getOverrides(): array + { + return $this->overrides; + } + + /** + * Get all registered service overrides + * + * @return array> + */ + public function getRegisteredOverrides(): array + { + return $this->registeredOverrides; + } + + /** + * Check if a service override is present + * + * @param string $class + * + * @return bool + */ + public function hasOverride(string $class): bool + { + return isset($this->overrides[$class]); + } + + /** + * Check if a service override has been registered + * + * @param string $class + * + * @return bool + */ + public function hasRegisteredOverride(string $class): bool + { + return in_array($class, $this->registeredOverrides, true); + } + + /** + * Get all service overrides for a tenancy + * + * @param \Sprout\Contracts\Tenancy|null $tenancy + * + * @return \Sprout\Contracts\ServiceOverride[]|void + */ + public function getCurrentOverrides(?Tenancy $tenancy = null) + { + $tenancy ??= $this->getCurrentTenancy(); + + if ($tenancy !== null) { + return array_filter( + $this->overrides, + function (string $overrideClass) use ($tenancy) { + return $this->hasSetupOverride($tenancy, $overrideClass); + }, + ARRAY_FILTER_USE_KEY + ); + } + } +} diff --git a/src/Contracts/DeferrableServiceOverride.php b/src/Contracts/DeferrableServiceOverride.php new file mode 100644 index 0000000..03dabff --- /dev/null +++ b/src/Contracts/DeferrableServiceOverride.php @@ -0,0 +1,20 @@ +sprout->getOverrides() as $override) { - $override->cleanup($event->tenancy, $event->previous); - } + $this->sprout->cleanupOverrides($event->tenancy, $event->previous); } } diff --git a/src/Listeners/SetupServiceOverrides.php b/src/Listeners/SetupServiceOverrides.php index f3395af..1f9a314 100644 --- a/src/Listeners/SetupServiceOverrides.php +++ b/src/Listeners/SetupServiceOverrides.php @@ -47,8 +47,6 @@ public function handle(CurrentTenantChanged $event): void return; } - foreach ($this->sprout->getOverrides() as $override) { - $override->setup($event->tenancy, $event->current); - } + $this->sprout->setupOverrides($event->tenancy, $event->current); } } diff --git a/src/Overrides/CacheOverride.php b/src/Overrides/CacheOverride.php index 69bc48c..a720d10 100644 --- a/src/Overrides/CacheOverride.php +++ b/src/Overrides/CacheOverride.php @@ -3,7 +3,6 @@ namespace Sprout\Overrides; -use Illuminate\Cache\ApcStore; use Illuminate\Cache\ApcWrapper; use Illuminate\Cache\ArrayStore; use Illuminate\Cache\CacheManager; @@ -14,6 +13,7 @@ use Illuminate\Cache\RedisStore; use Illuminate\Contracts\Foundation\Application; use Sprout\Contracts\BootableServiceOverride; +use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Exceptions\MisconfigurationException; @@ -28,7 +28,7 @@ * * @package Overrides */ -final class CacheOverride implements BootableServiceOverride +final class CacheOverride implements BootableServiceOverride, DeferrableServiceOverride { /** * Cache stores that can be purged @@ -37,6 +37,16 @@ final class CacheOverride implements BootableServiceOverride */ private static array $purgableStores = []; + /** + * Get the service to watch for before overriding + * + * @return string + */ + public static function service(): string + { + return CacheManager::class; + } + /** * Boot a service override * diff --git a/src/Overrides/CookieOverride.php b/src/Overrides/CookieOverride.php index 5949c8a..8aa1c22 100644 --- a/src/Overrides/CookieOverride.php +++ b/src/Overrides/CookieOverride.php @@ -5,6 +5,7 @@ use Illuminate\Cookie\CookieJar; use Sprout\Concerns\OverridesCookieSettings; +use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\ServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; @@ -17,10 +18,20 @@ * * @package Overrides */ -final class CookieOverride implements ServiceOverride +final class CookieOverride implements ServiceOverride, DeferrableServiceOverride { use OverridesCookieSettings; + /** + * Get the service to watch for before overriding + * + * @return string + */ + public static function service(): string + { + return 'cookie'; + } + /** * Set up the service override * diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index 0ddb6e5..f56332a 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -10,6 +10,7 @@ use Illuminate\Session\SessionManager; use Sprout\Concerns\OverridesCookieSettings; use Sprout\Contracts\BootableServiceOverride; +use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Contracts\TenantHasResources; @@ -28,7 +29,7 @@ * * @package Overrides */ -final class SessionOverride implements BootableServiceOverride +final class SessionOverride implements BootableServiceOverride, DeferrableServiceOverride { use OverridesCookieSettings; @@ -47,6 +48,16 @@ public static function doNotOverrideDatabase(): void self::$overrideDatabase = false; } + /** + * Get the service to watch for before overriding + * + * @return string + */ + public static function service(): string + { + return SessionManager::class; + } + /** * Boot a service override * diff --git a/src/Overrides/StorageOverride.php b/src/Overrides/StorageOverride.php index 66f077b..61a8cfa 100644 --- a/src/Overrides/StorageOverride.php +++ b/src/Overrides/StorageOverride.php @@ -9,6 +9,7 @@ use Illuminate\Filesystem\FilesystemManager; use RuntimeException; use Sprout\Contracts\BootableServiceOverride; +use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Contracts\TenantHasResources; @@ -24,8 +25,18 @@ * * @package Overrides */ -final class StorageOverride implements BootableServiceOverride +final class StorageOverride implements BootableServiceOverride, DeferrableServiceOverride { + /** + * Get the service to watch for before overriding + * + * @return string + */ + public static function service(): string + { + return 'filesystem'; + } + /** * Boot a service override * diff --git a/src/Sprout.php b/src/Sprout.php index 3573fb5..f8beb82 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -4,6 +4,10 @@ namespace Sprout; use Illuminate\Contracts\Foundation\Application; +use InvalidArgumentException; +use Sprout\Concerns\HandlesServiceOverrides; +use Sprout\Contracts\BootableServiceOverride; +use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\ServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Managers\IdentityResolverManager; @@ -20,6 +24,8 @@ */ final class Sprout { + use HandlesServiceOverrides; + /** * @var \Illuminate\Contracts\Foundation\Application */ @@ -30,11 +36,6 @@ final class Sprout */ private array $tenancies = []; - /** - * @var array, \Sprout\Contracts\ServiceOverride> - */ - private array $overrides = []; - /** * @var bool */ @@ -165,42 +166,6 @@ public function tenancies(): TenancyManager return $this->app->make(TenancyManager::class); } - /** - * Is an override enabled - * - * @param string $class - * - * @return bool - */ - public function hasOverride(string $class): bool - { - return isset($this->overrides[$class]); - } - - /** - * Add an override - * - * @param \Sprout\Contracts\ServiceOverride $override - * - * @return $this - */ - public function addOverride(ServiceOverride $override): self - { - $this->overrides[$override::class] = $override; - - return $this; - } - - /** - * Get all overrides - * - * @return array, \Sprout\Contracts\ServiceOverride> - */ - public function getOverrides(): array - { - return $this->overrides; - } - /** * Check if a resolution hook is enabled * @@ -247,7 +212,7 @@ public function maskAsOutsideContext(): self * * @return bool */ - public function withinContext():bool + public function withinContext(): bool { return $this->withinContext; } diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index 897b2f4..eef288e 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -104,14 +104,7 @@ private function registerServiceOverrides(): void $overrides = config('sprout.services', []); foreach ($overrides as $overrideClass) { - if (! is_subclass_of($overrideClass, ServiceOverride::class)) { - throw new InvalidArgumentException('Provided class [' . $overrideClass . '] does not implement ' . ServiceOverride::class); - } - - /** @var \Sprout\Contracts\ServiceOverride $override */ - $override = $this->app->make($overrideClass); - - $this->sprout->addOverride($override); + $this->sprout->registerOverride($overrideClass); } } @@ -141,10 +134,6 @@ private function registerTenancyBootstrappers(): void private function bootServiceOverrides(): void { - foreach ($this->sprout->getOverrides() as $override) { - if ($override instanceof BootableServiceOverride) { - $override->boot($this->app, $this->sprout); - } - } + $this->sprout->bootOverrides(); } } diff --git a/tests/Http/Resolvers/SessionResolverTest.php b/tests/Http/Resolvers/SessionResolverTest.php index 7403592..bb54485 100644 --- a/tests/Http/Resolvers/SessionResolverTest.php +++ b/tests/Http/Resolvers/SessionResolverTest.php @@ -109,7 +109,7 @@ public function throwsExceptionWithoutHeader(): void #[Test] public function throwsExceptionIfSessionOverrideIsEnabled(): void { - sprout()->addOverride(new SessionOverride()); + sprout()->registerOverride(SessionOverride::class); $tenant = TenantModel::factory()->createOne(); $result = $this->withSession(['multitenancy' => ['tenants' => $tenant->getTenantIdentifier()]])->get(route('session.route')); diff --git a/tests/Overrides/CookieOverrideTest.php b/tests/Overrides/CookieOverrideTest.php index e69901c..3a47a24 100644 --- a/tests/Overrides/CookieOverrideTest.php +++ b/tests/Overrides/CookieOverrideTest.php @@ -21,6 +21,7 @@ use Sprout\Overrides\SessionOverride; use Sprout\Overrides\StorageOverride; use Workbench\App\Models\TenantModel; +use function Sprout\sprout; #[Group('services'), Group('cookies')] class CookieOverrideTest extends TestCase @@ -45,7 +46,6 @@ protected function noCookieOverride($app): void JobOverride::class, CacheOverride::class, AuthOverride::class, - SessionOverride::class, ]); }); } diff --git a/tests/Overrides/StorageOverrideTest.php b/tests/Overrides/StorageOverrideTest.php index b7acb62..98e8244 100644 --- a/tests/Overrides/StorageOverrideTest.php +++ b/tests/Overrides/StorageOverrideTest.php @@ -20,8 +20,10 @@ use Sprout\Overrides\CookieOverride; use Sprout\Overrides\JobOverride; use Sprout\Overrides\SessionOverride; +use Sprout\Overrides\StorageOverride; use Workbench\App\Models\NoResourcesTenantModel; use Workbench\App\Models\TenantModel; +use function Sprout\sprout; #[Group('services'), Group('filesystem')] class StorageOverrideTest extends TestCase @@ -61,6 +63,20 @@ protected function noStorageOverride($app): void }); } + protected function yesStorageOverride($app): void + { + tap($app['config'], static function (Repository $config) { + $config->set('sprout.services', [ + JobOverride::class, + CacheOverride::class, + AuthOverride::class, + CookieOverride::class, + SessionOverride::class, + StorageOverride::class + ]); + }); + } + #[Test, DefineEnvironment('createTenantDisk')] public function canCreateScopedTenantFilesystemDisk(): void { @@ -116,7 +132,11 @@ public function cleansUpStorageDiskAfterTenantChange(): void { $tenant = TenantModel::factory()->createOne(); - app(TenancyManager::class)->get()->setTenant($tenant); + $tenancy = app(TenancyManager::class)->get(); + + sprout()->setCurrentTenancy($tenancy); + + $tenancy->setTenant($tenant); Storage::disk('tenant'); @@ -130,7 +150,11 @@ public function recreatesStorageDiskPerTenant(): void { $tenant1 = TenantModel::factory()->createOne(); - app(TenancyManager::class)->get()->setTenant($tenant1); + $tenancy = app(TenancyManager::class)->get(); + + sprout()->setCurrentTenancy($tenancy); + + $tenancy->setTenant($tenant1); $disk = Storage::disk('tenant');