diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index a1e681d7..c1acb50b 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -247,7 +247,24 @@ class TenancyServiceProvider extends ServiceProvider /** @var CloneRoutesAsTenant $cloneRoutes */ $cloneRoutes = $this->app->make(CloneRoutesAsTenant::class); - /** See CloneRoutesAsTenant for usage details. */ + // The cloning action has two modes: + // 1. Clone all routes that have the middleware present in the action's $cloneRoutesWithMiddleware property. + // You can customize the middleware that triggers cloning by using cloneRoutesWithMiddleware() on the action. + // + // By default, the middleware is ['clone'], but using $cloneRoutes->cloneRoutesWithMiddleware(['clone', 'universal'])->handle() + // will clone all routes that have either 'clone' or 'universal' middleware (mentioning 'universal' since that's a common use case). + // + // Also, you can use the shouldClone() method to provide a custom closure that determines if a route should be cloned. + // + // 2. Clone only the routes that were manually added to the action using cloneRoute(). + // + // Regardless of the mode, you can provide a custom closure for defining the cloned route, e.g.: + // $cloneRoutesAction->cloneUsing(function (Route $route) { + // RouteFacade::get('/cloned/' . $route->uri(), fn () => 'cloned route')->name('cloned.' . $route->getName()); + // })->handle(); + // This will make all cloned routes use the custom closure to define the cloned route instead of the default behavior. + // See Stancl\Tenancy\Actions\CloneRoutesAsTenant for more details. + $cloneRoutes->handle(); } diff --git a/assets/config.php b/assets/config.php index 2a3a07e2..f15a843a 100644 --- a/assets/config.php +++ b/assets/config.php @@ -64,7 +64,7 @@ return [ * Only relevant if you're using the domain or subdomain identification middleware. */ 'central_domains' => [ - str(env('APP_URL'))->after('://')->before('/')->before(':')->toString(), + str(env('APP_URL'))->after('://')->before('/')->toString(), ], /** diff --git a/phpstan.neon b/phpstan.neon index bb97e3a0..2c6e3d69 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -46,6 +46,10 @@ parameters: message: '#PHPDoc tag \@param has invalid value \(dynamic#' paths: - src/helpers.php + - + message: '#Illuminate\\Routing\\UrlGenerator#' + paths: + - src/Bootstrappers/FilesystemTenancyBootstrapper.php - '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#' - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#' - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#' diff --git a/src/Actions/CloneRoutesAsTenant.php b/src/Actions/CloneRoutesAsTenant.php index 6e988907..f1cb1450 100644 --- a/src/Actions/CloneRoutesAsTenant.php +++ b/src/Actions/CloneRoutesAsTenant.php @@ -30,8 +30,6 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; * By providing a callback to shouldClone(), you can change how it's determined if a route should be cloned if you don't want to use middleware flags. * * Cloned routes are prefixed with '/{tenant}', flagged with 'tenant' middleware, and have their names prefixed with 'tenant.'. - * The addition of the 'tenant' middleware can be controlled using addTenantMiddleware(array). You can specify the identification - * middleware to be used on the cloned route using that method -- instead of using the approach that "inherits" it from a universal route. * * The addition of the tenant parameter can be controlled using addTenantParameter(true|false). Note that if you decide to disable * tenant parameter addition, the routes MUST differ in domains. This can be controlled using the domain(string|null) method. The @@ -41,7 +39,7 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; * Routes with names that are already prefixed won't be cloned - but that's just the default behavior. * The cloneUsing() method allows you to completely override the default behavior and customize how the cloned routes will be defined. * - * After cloning, only top-level middleware in $cloneRoutesWithMiddleware (as well as any route context flags) will be removed + * After cloning, only top-level middleware in $cloneRoutesWithMiddleware will be removed * from the new route (so by default, 'clone' will be omitted from the new route's MW). * Middleware groups are preserved as-is, even if they contain cloning middleware. * @@ -73,7 +71,7 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; * // cloned route can be customized using domain(string|null). By default, the cloned route will not be scoped to a domain, * // unless a domain() call is used. It's important to keep in mind that: * // 1. When addTenantParameter(false) is used, the paths will be the same, thus domains must differ. - * // 2. If the original route has no domain, the cloned route will override the original route as they will directly conflict. + * // 2. If the original route (with the same path) has no domain, the cloned route will never be used due to registration order. * $cloneAction->addTenantParameter(false)->cloneRoutesWithMiddleware(['clone'])->cloneRoute('no-tenant-parameter')->handle(); * ``` * @@ -86,50 +84,27 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; */ class CloneRoutesAsTenant { - /** @var list */ protected array $routesToClone = []; - protected bool $addTenantParameter = true; protected bool $tenantParameterBeforePrefix = true; protected string|null $domain = null; - - /** - * The callback should accept a Route instance or the route name (string). - * - * @var ?Closure(Route|string): void - */ - protected Closure|null $cloneUsing = null; - - /** @var ?Closure(Route): bool */ + protected Closure|null $cloneUsing = null; // The callback should accept Route instance or the route name (string) protected Closure|null $shouldClone = null; - - /** @var list */ protected array $cloneRoutesWithMiddleware = ['clone']; - /** @var list */ - protected array $addTenantMiddleware = ['tenant']; - public function __construct( protected Router $router, ) {} - public static function make(): static - { - return app(static::class); - } - /** Clone routes. This resets routesToClone() but not other config. */ public function handle(): void { // If no routes were specified using cloneRoute(), get all routes // and for each, determine if it should be cloned if (! $this->routesToClone) { - /** @var list */ - $routesToClone = collect($this->router->getRoutes()->get()) + $this->routesToClone = collect($this->router->getRoutes()->get()) ->filter(fn (Route $route) => $this->shouldBeCloned($route)) ->all(); - - $this->routesToClone = $routesToClone; } foreach ($this->routesToClone as $route) { @@ -143,9 +118,7 @@ class CloneRoutesAsTenant if (is_string($route)) { $this->router->getRoutes()->refreshNameLookups(); - $routeName = $route; - $route = $this->router->getRoutes()->getByName($routeName); - assert(! is_null($route), "Route [{$routeName}] was meant to be cloned but does not exist."); + $route = $this->router->getRoutes()->getByName($route); } $this->createNewRoute($route); @@ -170,20 +143,6 @@ class CloneRoutesAsTenant return $this; } - /** - * The tenant middleware to be added to the cloned route. - * - * If used with early identification, make sure to include 'tenant' in this array. - * - * @param list $middleware - */ - public function addTenantMiddleware(array $middleware): static - { - $this->addTenantMiddleware = $middleware; - - return $this; - } - /** The domain the cloned route should use. Set to null if it shouldn't be scoped to a domain. */ public function domain(string|null $domain): static { @@ -192,11 +151,7 @@ class CloneRoutesAsTenant return $this; } - /** - * Provide a custom callback for cloning routes, instead of the default behavior. - * - * @param ?Closure(Route|string): void $cloneUsing - */ + /** Provide a custom callback for cloning routes, instead of the default behavior. */ public function cloneUsing(Closure|null $cloneUsing): static { $this->cloneUsing = $cloneUsing; @@ -204,11 +159,7 @@ class CloneRoutesAsTenant return $this; } - /** - * Specify which middleware should serve as "flags" telling this action to clone those routes. - * - * @param list $middleware - */ + /** Specify which middleware should serve as "flags" telling this action to clone those routes. */ public function cloneRoutesWithMiddleware(array $middleware): static { $this->cloneRoutesWithMiddleware = $middleware; @@ -219,9 +170,7 @@ class CloneRoutesAsTenant /** * Provide a custom callback for determining whether a route should be cloned. * Overrides the default middleware-based detection. - * - * @param Closure(Route): bool $shouldClone - */ + * */ public function shouldClone(Closure|null $shouldClone): static { $this->shouldClone = $shouldClone; @@ -244,18 +193,6 @@ class CloneRoutesAsTenant return $this; } - /** - * Clone individual routes. - * - * @param list $routes - */ - public function cloneRoutes(array $routes): static - { - $this->routesToClone = array_merge($this->routesToClone, $routes); - - return $this; - } - protected function shouldBeCloned(Route $route): bool { // Don't clone routes that already have tenant parameter or prefix @@ -321,15 +258,17 @@ class CloneRoutesAsTenant return $newRoute; } - /** Removes top-level cloneRoutesWithMiddleware and context flags, adds 'tenant' middleware. */ + /** Removes top-level cloneRoutesWithMiddleware and adds 'tenant' middleware. */ protected function processMiddlewareForCloning(array $middleware): array { $processedMiddleware = array_filter( $middleware, - fn ($mw) => ! in_array($mw, $this->cloneRoutesWithMiddleware) && ! in_array($mw, ['central', 'tenant', 'universal']) + fn ($mw) => ! in_array($mw, $this->cloneRoutesWithMiddleware) ); - return array_unique(array_merge($processedMiddleware, $this->addTenantMiddleware)); + $processedMiddleware[] = 'tenant'; + + return array_unique($processedMiddleware); } /** Check if route already has tenant parameter or name prefix. */ diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index faa02de7..2c2d9ec9 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; use Illuminate\Foundation\Application; +use Illuminate\Routing\UrlGenerator; use Illuminate\Session\FileSessionHandler; use Illuminate\Support\Facades\Storage; use Stancl\Tenancy\Contracts\TenancyBootstrapper; @@ -21,6 +22,13 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper ) { $this->originalAssetUrl = $this->app['config']['app.asset_url']; $this->originalStoragePath = $app->storagePath(); + + $this->app['url']->macro('setAssetRoot', function ($root) { + /** @var UrlGenerator $this */ + $this->assetRoot = $root; + + return $this; + }); } public function bootstrap(Tenant $tenant): void @@ -99,16 +107,16 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper if ($suffix === false) { $this->app['config']['app.asset_url'] = $this->originalAssetUrl; - $this->app['url']->useAssetOrigin($this->originalAssetUrl); + $this->app['url']->setAssetRoot($this->originalAssetUrl); return; } if ($this->originalAssetUrl) { $this->app['config']['app.asset_url'] = $this->originalAssetUrl . "/$suffix"; - $this->app['url']->useAssetOrigin($this->app['config']['app.asset_url']); + $this->app['url']->setAssetRoot($this->app['config']['app.asset_url']); } else { - $this->app['url']->useAssetOrigin($this->app['url']->route('stancl.tenancy.asset', ['path' => ''])); + $this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => ''])); } } diff --git a/src/Concerns/ManagesRLSPolicies.php b/src/Concerns/ManagesRLSPolicies.php index f6329d0e..6b804fb7 100644 --- a/src/Concerns/ManagesRLSPolicies.php +++ b/src/Concerns/ManagesRLSPolicies.php @@ -26,7 +26,7 @@ trait ManagesRLSPolicies $policies = static::getRLSPolicies($table); foreach ($policies as $policy) { - DB::statement("DROP POLICY {$policy} ON {$table}"); + DB::statement('DROP POLICY ? ON ?', [$policy, $table]); } return count($policies); diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index afd20fb6..9b32f088 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -6,7 +6,6 @@ namespace Stancl\Tenancy; use Closure; use Illuminate\Cache\CacheManager; -use Illuminate\Contracts\Container\Container; use Illuminate\Database\Console\Migrations\FreshCommand; use Illuminate\Routing\Events\RouteMatched; use Illuminate\Support\Facades\Event; @@ -158,13 +157,12 @@ class TenancyServiceProvider extends ServiceProvider $this->loadRoutesFrom(__DIR__ . '/../assets/routes.php'); } - $this->app->singleton('globalUrl', function (Container $app) { + $this->app->singleton('globalUrl', function ($app) { if ($app->bound(FilesystemTenancyBootstrapper::class)) { - /** @var \Illuminate\Routing\UrlGenerator */ - $instance = clone $app->make('url'); - $instance->useAssetOrigin($app->make(FilesystemTenancyBootstrapper::class)->originalAssetUrl); + $instance = clone $app['url']; + $instance->setAssetRoot($app[FilesystemTenancyBootstrapper::class]->originalAssetUrl); } else { - $instance = $app->make('url'); + $instance = $app['url']; } return $instance; diff --git a/tests/CloneActionTest.php b/tests/CloneActionTest.php index ab9c5e9b..28a8ccd3 100644 --- a/tests/CloneActionTest.php +++ b/tests/CloneActionTest.php @@ -401,77 +401,3 @@ test('tenant parameter addition can be controlled by setting addTenantParameter' $this->withoutExceptionHandling()->get('http://central.localhost/foo')->assertSee('central'); } })->with([true, false]); - -test('existing context flags are removed during cloning', function () { - RouteFacade::get('/foo', fn () => true)->name('foo')->middleware(['clone', 'central']); - RouteFacade::get('/bar', fn () => true)->name('bar')->middleware(['clone', 'universal']); - - $cloneAction = app(CloneRoutesAsTenant::class); - - // Clone foo route - $cloneAction->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName()) - ->toContain('tenant.foo'); - expect(tenancy()->getRouteMiddleware(RouteFacade::getRoutes()->getByName('tenant.foo'))) - ->not()->toContain('central'); - - // Clone bar route - $cloneAction->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName()) - ->toContain('tenant.foo', 'tenant.bar'); - expect(tenancy()->getRouteMiddleware(RouteFacade::getRoutes()->getByName('tenant.foo'))) - ->not()->toContain('universal'); -}); - -test('cloning a route without a prefix or differing domains overrides the original route', function () { - RouteFacade::get('/foo', fn () => true)->name('foo')->middleware(['clone']); - - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('foo'); - - $cloneAction = CloneRoutesAsTenant::make(); - $cloneAction->cloneRoute('foo') - ->addTenantParameter(false) - ->tenantParameterBeforePrefix(false) - ->handle(); - - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.foo'); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->not()->toContain('foo'); -}); - -test('addTenantMiddleware can be used to specify the tenant middleware for the cloned route', function () { - RouteFacade::get('/foo', fn () => true)->name('foo')->middleware(['clone']); - RouteFacade::get('/bar', fn () => true)->name('bar')->middleware(['clone']); - - $cloneAction = app(CloneRoutesAsTenant::class); - - $cloneAction->cloneRoute('foo')->addTenantMiddleware([InitializeTenancyByPath::class])->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.foo'); - $cloned = RouteFacade::getRoutes()->getByName('tenant.foo'); - expect($cloned->uri())->toBe('{tenant}/foo'); - expect($cloned->getName())->toBe('tenant.foo'); - expect(tenancy()->getRouteMiddleware($cloned))->toBe([InitializeTenancyByPath::class]); - - $cloneAction->cloneRoute('bar') - ->addTenantMiddleware([InitializeTenancyByDomain::class]) - ->domain('foo.localhost') - ->addTenantParameter(false) - ->tenantParameterBeforePrefix(false) - ->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.bar'); - $cloned = RouteFacade::getRoutes()->getByName('tenant.bar'); - expect($cloned->uri())->toBe('bar'); - expect($cloned->getName())->toBe('tenant.bar'); - expect($cloned->getDomain())->toBe('foo.localhost'); - expect(tenancy()->getRouteMiddleware($cloned))->toBe([InitializeTenancyByDomain::class]); -}); - -test('cloneRoutes can be used to clone multiple routes', function () { - RouteFacade::get('/foo', fn () => true)->name('foo'); - $bar = RouteFacade::get('/bar', fn () => true)->name('bar'); - RouteFacade::get('/baz', fn () => true)->name('baz'); - - CloneRoutesAsTenant::make()->cloneRoutes(['foo', $bar])->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.foo'); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.bar'); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->not()->toContain('tenant.baz'); -}); diff --git a/tests/RLS/PolicyTest.php b/tests/RLS/PolicyTest.php index b790343e..ee9bf5cc 100644 --- a/tests/RLS/PolicyTest.php +++ b/tests/RLS/PolicyTest.php @@ -17,7 +17,6 @@ use Stancl\Tenancy\Commands\CreateUserWithRLSPolicies; use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager; use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager; use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper; -use Stancl\Tenancy\Tenancy; use function Stancl\Tenancy\Tests\pest; beforeEach(function () { @@ -190,22 +189,6 @@ test('rls command recreates policies if the force option is passed', function (s TraitRLSManager::class, ]); -test('dropRLSPolicies only drops RLS policies', function () { - DB::statement('CREATE POLICY "comments_dummy_rls_policy" ON comments USING (true)'); - DB::statement('CREATE POLICY "comments_foo_policy" ON comments USING (true)'); // non-RLS policy - - $policyCount = fn () => count(DB::select("SELECT policyname FROM pg_policies WHERE tablename = 'comments'")); - - expect($policyCount())->toBe(2); - - $removed = Tenancy::dropRLSPolicies('comments'); - - expect($removed)->toBe(1); - - // Only the non-RLS policy remains - expect($policyCount())->toBe(1); -}); - test('queries will stop working when the tenant session variable is not set', function(string $manager, bool $forceRls) { CreateUserWithRLSPolicies::$forceRls = $forceRls;