diff --git a/README.md b/README.md index a2941cc..b69afc9 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,13 @@ ![Packagist Version](https://img.shields.io/packagist/v/sprout/sprout) ![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/sprout/sprout) ![GitHub](https://img.shields.io/github/license/sprout-laravel/sprout) -![Psalm Level](https://shepherd.dev/github/sprout-laravel/sprout/level.svg) ![Laravel](https://img.shields.io/badge/laravel-10.x-red.svg) Main: [![codecov](https://codecov.io/gh/sprout-laravel/sprout/branch/main/graph/badge.svg?token=FHJ41NQMTA)](https://codecov.io/gh/sprout-laravel/sprout) -[![CircleCI](https://circleci.com/gh/sprout-laravel/sprout/tree/main.svg?style=shield)](https://circleci.com/gh/sprout-laravel/sprout/tree/main) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fsmplphp%2Fcore%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/sprout-laravel/sprout/main) -Develop: - -[![codecov](https://codecov.io/gh/sprout-laravel/sprout/branch/develop/graph/badge.svg?token=FHJ41NQMTA)](https://codecov.io/gh/sprout-laravel/sprout) -[![CircleCI](https://circleci.com/gh/sprout-laravel/sprout/tree/develop.svg?style=shield)](https://circleci.com/gh/sprout-laravel/sprout/tree/develop) -[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fsmplphp%2Fcore%2Fdevelop)](https://dashboard.stryker-mutator.io/reports/github.com/sprout-laravel/sprout/develop) - # Sprout for Laravel ### A flexible, seamless and easy to use multitenancy solution for Laravel diff --git a/composer.json b/composer.json index 6a5f5e0..b4530cb 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "require-dev" : { "phpunit/phpunit" : "^11.0.1", "orchestra/testbench": "^9.4", - "larastan/larastan" : "^2.9" + "larastan/larastan" : "^2.9", + "infection/infection": "^0.29.6" }, "license" : "MIT", "autoload" : { @@ -60,6 +61,9 @@ ], "test" : [ "@php vendor/bin/phpunit" + ], + "mutation" : [ + "@php vendor/bin/infection" ] }, "extra" : { @@ -68,5 +72,10 @@ "Sprout\\SproutServiceProvider" ] } + }, + "config" : { + "allow-plugins": { + "infection/extension-installer": true + } } } diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..e7d925e --- /dev/null +++ b/infection.json5 @@ -0,0 +1,14 @@ +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "build/infection.log" + }, + "mutators": { + "@default": true + } +} \ No newline at end of file diff --git a/src/Database/Eloquent/Concerns/IsTenantModel.php b/src/Database/Eloquent/Concerns/IsTenantModel.php index 711ee0b..19ca3e1 100644 --- a/src/Database/Eloquent/Concerns/IsTenantModel.php +++ b/src/Database/Eloquent/Concerns/IsTenantModel.php @@ -15,6 +15,8 @@ trait IsTenantModel * Retrieve the identifier used to publicly identify the tenant. * * @return string + * + * @infection-ignore-all */ public function getTenantIdentifier(): string { @@ -29,6 +31,8 @@ public function getTenantIdentifier(): string * Used primarily by {@see \Sprout\Contracts\TenantProvider}. * * @return string + * + * @infection-ignore-all */ public function getTenantIdentifierName(): string { @@ -41,6 +45,8 @@ public function getTenantIdentifierName(): string * Retrieve the key used to identify a tenant internally. * * @return int|string + * + * @infection-ignore-all */ public function getTenantKey(): int|string { @@ -55,6 +61,8 @@ public function getTenantKey(): int|string * Used primarily by {@see \Sprout\Contracts\TenantProvider}. * * @return string + * + * @infection-ignore-all */ public function getTenantKeyName(): string { diff --git a/src/Sprout.php b/src/Sprout.php index 2a4bab5..3ab74a2 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -27,7 +27,7 @@ public function __construct(Application $app) $this->app = $app; } - public function config(string $key, mixed $default): mixed + public function config(string $key, mixed $default = null): mixed { return $this->app->make('config')->get('sprout.' . $key, $default); } @@ -89,7 +89,7 @@ public function contextKey(Tenancy $tenancy): string public function contextValue(Tenant $tenant): int|string { - return $this->config('context.user', 'key') === 'key' + return $this->config('context.use', 'key') === 'key' ? $tenant->getTenantKey() : $tenant->getTenantIdentifier(); } diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index 62ad058..a07aaec 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -14,6 +14,7 @@ use Sprout\Listeners\PerformIdentityResolverSetup; use Sprout\Managers\IdentityResolverManager; use Sprout\Managers\ProviderManager; +use Sprout\Managers\TenancyManager; class SproutServiceProvider extends ServiceProvider { @@ -55,9 +56,15 @@ private function registerManagers(): void return new IdentityResolverManager($app); }); + // Register the tenancy manager + $this->app->singleton(TenancyManager::class, function ($app) { + return new TenancyManager($app, $app->make(ProviderManager::class)); + }); + // Alias the managers with simple names $this->app->alias(ProviderManager::class, 'sprout.providers'); $this->app->alias(IdentityResolverManager::class, 'sprout.resolvers'); + $this->app->alias(TenancyManager::class, 'sprout.tenancies'); } private function registerMiddleware(): void diff --git a/tests/Core/ServiceProviderTest.php b/tests/Core/ServiceProviderTest.php new file mode 100644 index 0000000..c3893a3 --- /dev/null +++ b/tests/Core/ServiceProviderTest.php @@ -0,0 +1,111 @@ +assertTrue(app()->providerIsLoaded(SproutServiceProvider::class)); + } + + #[Test] + public function sproutIsRegistered(): void + { + $this->assertTrue(app()->has(Sprout::class)); + $this->assertTrue(app()->has('sprout')); + $this->assertTrue(app()->isShared(Sprout::class)); + $this->assertFalse(app()->isShared('sprout')); + + $this->assertSame(app()->make(Sprout::class), app()->make(Sprout::class)); + $this->assertSame(app()->make('sprout'), app()->make('sprout')); + $this->assertSame(app()->make(Sprout::class), app()->make('sprout')); + $this->assertSame(app()->make('sprout'), app()->make(Sprout::class)); + } + + #[Test] + public function coreSproutConfigExists(): void + { + $this->assertTrue(app()['config']->has('sprout')); + $this->assertIsArray(app()['config']->get('sprout')); + $this->assertTrue(app()['config']->has('sprout.listen_for_routing')); + $this->assertTrue(app()['config']->has('sprout.context')); + $this->assertTrue(app()['config']->has('sprout.context.key')); + $this->assertTrue(app()['config']->has('sprout.context.use')); + } + + #[Test] + public function providerManagerIsRegistered(): void + { + $this->assertTrue(app()->has(ProviderManager::class)); + $this->assertTrue(app()->has('sprout.providers')); + $this->assertTrue(app()->isShared(ProviderManager::class)); + $this->assertFalse(app()->isShared('sprout.providers')); + + $this->assertSame(app()->make(ProviderManager::class), app()->make(ProviderManager::class)); + $this->assertSame(app()->make('sprout.providers'), app()->make('sprout.providers')); + $this->assertSame(app()->make(ProviderManager::class), app()->make('sprout.providers')); + $this->assertSame(app()->make('sprout.providers'), app()->make(ProviderManager::class)); + $this->assertSame(app()->make(Sprout::class)->providers(), app()->make('sprout.providers')); + $this->assertSame(app()->make(Sprout::class)->providers(), app()->make(ProviderManager::class)); + } + + #[Test] + public function identityResolverManagerIsRegistered(): void + { + $this->assertTrue(app()->has(IdentityResolverManager::class)); + $this->assertTrue(app()->has('sprout.resolvers')); + $this->assertTrue(app()->isShared(IdentityResolverManager::class)); + $this->assertFalse(app()->isShared('sprout.resolvers')); + + $this->assertSame(app()->make(IdentityResolverManager::class), app()->make(IdentityResolverManager::class)); + $this->assertSame(app()->make('sprout.resolvers'), app()->make('sprout.resolvers')); + $this->assertSame(app()->make(IdentityResolverManager::class), app()->make('sprout.resolvers')); + $this->assertSame(app()->make('sprout.resolvers'), app()->make(IdentityResolverManager::class)); + $this->assertSame(app()->make(Sprout::class)->resolvers(), app()->make('sprout.resolvers')); + $this->assertSame(app()->make(Sprout::class)->resolvers(), app()->make(IdentityResolverManager::class)); + } + + #[Test] + public function tenancyManagerIsRegistered(): void + { + $this->assertTrue(app()->has(TenancyManager::class)); + $this->assertTrue(app()->has('sprout.tenancies')); + $this->assertTrue(app()->isShared(TenancyManager::class)); + $this->assertFalse(app()->isShared('sprout.tenancies')); + + $this->assertSame(app()->make(TenancyManager::class), app()->make(TenancyManager::class)); + $this->assertSame(app()->make('sprout.tenancies'), app()->make('sprout.tenancies')); + $this->assertSame(app()->make(TenancyManager::class), app()->make('sprout.tenancies')); + $this->assertSame(app()->make('sprout.tenancies'), app()->make(TenancyManager::class)); + $this->assertSame(app()->make(Sprout::class)->tenancies(), app()->make('sprout.tenancies')); + $this->assertSame(app()->make(Sprout::class)->tenancies(), app()->make(TenancyManager::class)); + } + + #[Test] + public function publishesConfig(): void + { + $paths = ServiceProvider::pathsToPublish(SproutServiceProvider::class, 'config'); + + $key = realpath(__DIR__ . '/../../src'); + + $this->assertArrayHasKey($key . '/../resources/config/multitenancy.php', $paths); + $this->assertContains(config_path('multitenancy.php'), $paths); + } +} diff --git a/tests/Core/SproutTest.php b/tests/Core/SproutTest.php new file mode 100644 index 0000000..29bf01d --- /dev/null +++ b/tests/Core/SproutTest.php @@ -0,0 +1,113 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + #[Test] + public function makesCoreConfigAccessible(): void + { + $sprout = app()->make(Sprout::class); + + $this->assertTrue($sprout->config('listen_for_routing')); + $this->assertTrue(config('sprout.listen_for_routing')); + $this->assertNotNull($sprout->config('context')); + $this->assertNotNull(config('sprout.context')); + + app()['config']->set('sprout.listen_for_routing', false); + + $this->assertFalse($sprout->config('listen_for_routing')); + $this->assertFalse(config('sprout.listen_for_routing')); + } + + #[Test] + public function hasHelperForListeningToRoutingEvents(): void + { + $sprout = app()->make(Sprout::class); + + app()['config']->set('sprout.listen_for_routing', false); + + $this->assertFalse($sprout->config('listen_for_routing')); + $this->assertFalse(config('sprout.listen_for_routing')); + $this->assertFalse($sprout->shouldListenForRouting()); + + app()['config']->set('sprout.listen_for_routing', true); + + $this->assertTrue($sprout->config('listen_for_routing')); + $this->assertTrue(config('sprout.listen_for_routing')); + $this->assertTrue($sprout->shouldListenForRouting()); + } + + #[Test] + public function canProvideContextKeyForTenancy(): void + { + $sprout = app()->make(Sprout::class); + $tenancy = $sprout->tenancies()->get('tenants'); + + app()['config']->set('sprout.context.key', '{tenancy}_key'); + + $this->assertSame('tenants_key', $sprout->contextKey($tenancy)); + + app()['config']->set('sprout.context.key', 'the_key_for_the_{tenancy}'); + + $this->assertSame('the_key_for_the_tenants', $sprout->contextKey($tenancy)); + } + + #[Test] + public function canProvideContextValueForTenant(): void + { + $sprout = app()->make(Sprout::class); + $tenant = TenantModel::first(); + + app()['config']->set('sprout.context.use', 'key'); + + $this->assertSame($tenant->getTenantKey(), $sprout->contextValue($tenant)); + + app()['config']->set('sprout.context.use', 'identifier'); + + $this->assertSame($tenant->getTenantIdentifier(), $sprout->contextValue($tenant)); + } + + #[Test] + public function keepsTrackOfCurrentTenancies(): void + { + $sprout = app()->make(Sprout::class); + + $this->assertFalse($sprout->hasCurrentTenancy()); + $this->assertNull($sprout->getCurrentTenancy()); + $this->assertEmpty($sprout->getAllCurrentTenancies()); + + $tenancy = $sprout->tenancies()->get('tenants'); + $sprout->setCurrentTenancy($tenancy); + + $this->assertTrue($sprout->hasCurrentTenancy()); + $this->assertNotNull($sprout->getCurrentTenancy()); + $this->assertSame($tenancy, $sprout->getCurrentTenancy()); + $this->assertNotEmpty($sprout->getAllCurrentTenancies()); + $this->assertCount(1, $sprout->getAllCurrentTenancies()); + + $sprout->setCurrentTenancy($tenancy); + + $this->assertCount(1, $sprout->getAllCurrentTenancies()); + } +}