From f771aa8645dac1e630b399e42c7621cff9ac838d Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 2 Jun 2025 19:05:17 +0200 Subject: [PATCH 1/6] [4.x] Test that route model binding works correctly with path identification (#1360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test that route model binding works with path identification (closure-based routes) * Correct test name * Update tests/PathIdentificationTest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * make assertions more clear --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Samuel Štancl --- tests/PathIdentificationTest.php | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php index 79cd3816..fd602a9f 100644 --- a/tests/PathIdentificationTest.php +++ b/tests/PathIdentificationTest.php @@ -3,15 +3,29 @@ declare(strict_types=1); use Illuminate\Contracts\Http\Kernel; +use Illuminate\Database\QueryException; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Facades\Event; use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException; use Stancl\Tenancy\Exceptions\TenantColumnNotWhitelistedException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException; use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Jobs\MigrateDatabase; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Listeners\RevertToCentralContext; +use Stancl\Tenancy\Tests\Etc\User; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Events\TenancyEnded; + use function Stancl\Tenancy\Tests\pest; beforeEach(function () { @@ -246,3 +260,38 @@ test('any extra model column needs to be whitelisted', function () { config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.allowed_extra_model_columns' => ['slug']]); pest()->get('/acme/foo')->assertSee($tenant->getTenantKey()); }); + +test('route model binding works with path identification', function() { + config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, MigrateDatabase::class, + ])->send(fn (TenantCreated $event) => $event->tenant)->toListener()); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + $tenant = Tenant::create(); + + $this->withoutExceptionHandling(); + + // Importantly, the route must have the 'web' middleware group, or SubstituteBindings directly + Route::get('/{tenant}/foo/{user}', fn (User $user) => $user->name)->middleware([InitializeTenancyByPath::class, 'web']); + Route::get('/{tenant}/bar/{user}', fn (User $user) => $user->name)->middleware([InitializeTenancyByPath::class, SubstituteBindings::class]); + + $user = $tenant->run(fn () => User::create(['name' => 'John Doe', 'email' => 'john@doe.com', 'password' => 'foobar'])); + + pest()->get("/{$tenant->id}/foo/{$user->id}")->assertSee("John Doe"); + tenancy()->end(); + pest()->get("/{$tenant->id}/bar/{$user->id}")->assertSee("John Doe"); + tenancy()->end(); + + // If SubstituteBindings comes BEFORE tenancy middleware and middleware priority is not set, route model binding is NOT expected to work correctly + // Since SubstituteBindings runs first, it tries to query the central database instead of the tenant database (which fails with a QueryException in this case) + Route::get('/{tenant}/baz/{user}', fn (User $user) => $user->name ?: 'No user')->middleware([SubstituteBindings::class, InitializeTenancyByPath::class]); + expect(fn () => pest()->get("/{$tenant->id}/baz/{$user->id}"))->toThrow(QueryException::class); + tenancy()->end(); + + // If SubstituteBindings is NOT USED AT ALL, we simply get an empty User instance + Route::get('/{tenant}/xyz/{user}', fn (User $user) => $user->name ?: 'No user')->middleware([InitializeTenancyByPath::class]); + pest()->get("/{$tenant->id}/xyz/{$user->id}")->assertSee('No user'); +}); From e74e1f92e12726b34739d7d2057a1a459a881927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Mon, 2 Jun 2025 20:34:49 +0200 Subject: [PATCH 2/6] Make RouteMode enum backed (#1362) --- .php-cs-fixer.php | 1 + src/Enums/RouteMode.php | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 15a75d64..c0fe775c 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -132,6 +132,7 @@ $finder = Finder::create() ->in([ $project_path . '/src', ]) + ->exclude('Enums') ->name('*.php') ->notName('*.blade.php') ->ignoreDotFiles(true) diff --git a/src/Enums/RouteMode.php b/src/Enums/RouteMode.php index b64e550c..b04833d9 100644 --- a/src/Enums/RouteMode.php +++ b/src/Enums/RouteMode.php @@ -4,9 +4,12 @@ declare(strict_types=1); namespace Stancl\Tenancy\Enums; -enum RouteMode +/** + * Note: The backing values are not part of the public API and are subject to change. + */ +enum RouteMode: int { - case TENANT; - case CENTRAL; - case UNIVERSAL; + case CENTRAL = 0b01; + case TENANT = 0b10; + case UNIVERSAL = 0b11; } From 2057e1e5ae469d305144d9e5f8a842d915d489f9 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 5 Jun 2025 05:06:05 +0200 Subject: [PATCH 3/6] [4.x] Make forcing RLS configurable (#1293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add `$forceRls` static property to tenants:rls * Set `$forceRls` in tests where scoping is tested, add non-superuser, non-bypassrls table owner test * Move DROP TABLE statement * Remove try/catch * Put DROP OWNED BY into try/catch * Static property cleanup in afterEach * Make with() matrix syntax more clear by using with() multiple times * Fix typo, improve comment * Move and update force RLS comment * Add test for `$forceRls = false`, refactor BYPASSRLS test * Update link in test comment * Add a dataset for `$forceRls` in the table owner test, fix BYPASSRLS test * Correct PR link comment * minor fixes * Add test that makes the bypassrls/forceRls behavior clear * Delete redundant test * cleanup * Update tests/RLS/TableManagerTest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Samuel Štancl Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Commands/CreateUserWithRLSPolicies.php | 28 ++-- tests/RLS/PolicyTest.php | 14 +- tests/RLS/TableManagerTest.php | 146 ++++++++++++++++++++- tests/RLS/TraitManagerTest.php | 13 +- 4 files changed, 181 insertions(+), 20 deletions(-) diff --git a/src/Commands/CreateUserWithRLSPolicies.php b/src/Commands/CreateUserWithRLSPolicies.php index aa171d58..420df935 100644 --- a/src/Commands/CreateUserWithRLSPolicies.php +++ b/src/Commands/CreateUserWithRLSPolicies.php @@ -22,6 +22,23 @@ class CreateUserWithRLSPolicies extends Command protected $description = "Creates RLS policies for all tables related to the tenant table. Also creates the RLS user if it doesn't exist yet"; + /** + * Force, rather than just enable, the created RLS policies. + * + * By default, table owners bypass RLS policies. When this is enabled, + * they also need the BYPASSRLS permission. If your setup lets you create + * a user with BYPASSRLS, you may prefer leaving this on for additional + * safety. Otherwise, if you can't use BYPASSRLS, you can set this to false + * and depend on the behavior of table owners bypassing RLS automatically. + * + * This setting generally doesn't affect behavior at all with "default" + * setups, however if you have a more custom setup, with additional users + * involved (e.g. central connection user not being the same user that + * creates tables, or the created "RLS user" creating some tables) you + * should take care with how you configure this. + */ + public static bool $forceRls = true; + public function handle(PermissionControlledPostgreSQLSchemaManager $manager): int { $username = config('tenancy.rls.user.username'); @@ -49,14 +66,9 @@ class CreateUserWithRLSPolicies extends Command // Enable RLS scoping on the table (without this, queries won't be scoped using RLS) DB::statement("ALTER TABLE {$table} ENABLE ROW LEVEL SECURITY"); - /** - * Force RLS scoping on the table, so that the table owner users - * don't bypass the scoping – table owners bypass RLS by default. - * - * E.g. when using a custom implementation where you create tables as the RLS user, - * the queries won't be scoped for the RLS user unless we force the RLS scoping using this query. - */ - DB::statement("ALTER TABLE {$table} FORCE ROW LEVEL SECURITY"); + if (static::$forceRls) { + DB::statement("ALTER TABLE {$table} FORCE ROW LEVEL SECURITY"); + } } /** diff --git a/tests/RLS/PolicyTest.php b/tests/RLS/PolicyTest.php index 7278776a..ee9bf5cc 100644 --- a/tests/RLS/PolicyTest.php +++ b/tests/RLS/PolicyTest.php @@ -20,6 +20,7 @@ use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper; use function Stancl\Tenancy\Tests\pest; beforeEach(function () { + CreateUserWithRLSPolicies::$forceRls = true; TraitRLSManager::$excludedModels = [Article::class]; TraitRLSManager::$modelDirectories = [__DIR__ . '/Etc']; @@ -79,6 +80,10 @@ beforeEach(function () { }); }); +afterEach(function () { + CreateUserWithRLSPolicies::$forceRls = true; +}); + // Regression test for https://github.com/archtechx/tenancy/pull/1280 test('rls command doesnt fail when a view is in the database', function (string $manager) { DB::statement(" @@ -184,7 +189,9 @@ test('rls command recreates policies if the force option is passed', function (s TraitRLSManager::class, ]); -test('queries will stop working when the tenant session variable is not set', function(string $manager) { +test('queries will stop working when the tenant session variable is not set', function(string $manager, bool $forceRls) { + CreateUserWithRLSPolicies::$forceRls = $forceRls; + config(['tenancy.rls.manager' => $manager]); $sessionVariableName = config('tenancy.rls.session_variable_name'); @@ -216,7 +223,4 @@ test('queries will stop working when the tenant session variable is not set', fu INSERT INTO posts (text, tenant_id, author_id) VALUES ('post2', ?, ?) SQL, [$tenant->id, $authorId]))->toThrow(QueryException::class); -})->with([ - TableRLSManager::class, - TraitRLSManager::class, -]); +})->with([TableRLSManager::class, TraitRLSManager::class])->with([true, false]); diff --git a/tests/RLS/TableManagerTest.php b/tests/RLS/TableManagerTest.php index fd9c6f44..c0520a1d 100644 --- a/tests/RLS/TableManagerTest.php +++ b/tests/RLS/TableManagerTest.php @@ -22,6 +22,7 @@ use Stancl\Tenancy\Database\Exceptions\RecursiveRelationshipException; use function Stancl\Tenancy\Tests\pest; beforeEach(function () { + CreateUserWithRLSPolicies::$forceRls = true; TableRLSManager::$scopeByDefault = true; Event::listen(TenancyInitialized::class, BootstrapTenancy::class); @@ -107,6 +108,10 @@ beforeEach(function () { }); }); +afterEach(function () { + CreateUserWithRLSPolicies::$forceRls = true; +}); + test('correct rls policies get created with the correct hash using table manager', function() { $manager = app(config('tenancy.rls.manager')); @@ -159,7 +164,9 @@ test('correct rls policies get created with the correct hash using table manager } }); -test('queries are correctly scoped using RLS', function() { +test('queries are correctly scoped using RLS', function (bool $forceRls) { + CreateUserWithRLSPolicies::$forceRls = $forceRls; + // 3-levels deep relationship Schema::create('notes', function (Blueprint $table) { $table->id(); @@ -320,7 +327,7 @@ test('queries are correctly scoped using RLS', function() { expect(fn () => DB::statement("INSERT INTO notes (text, comment_id) VALUES ('baz', {$post1Comment->id})")) ->toThrow(QueryException::class); -}); +})->with([true, false]); test('table rls manager generates relationship trees with tables related to the tenants table', function (bool $scopeByDefault) { TableRLSManager::$scopeByDefault = $scopeByDefault; @@ -535,6 +542,109 @@ test('table rls manager generates relationship trees with tables related to the ]); })->with([true, false]); +// https://github.com/archtechx/tenancy/pull/1293 +test('forceRls prevents even the table owner from querying his own tables if he doesnt have a BYPASSRLS permission', function (bool $forceRls) { + CreateUserWithRLSPolicies::$forceRls = $forceRls; + + // Drop all tables created in beforeEach + DB::statement("DROP TABLE authors, categories, posts, comments, reactions, articles;"); + + // Create a new user so we have full control over the permissions. + // We explicitly set bypassRls to false. + [$username, $password] = createPostgresUser('administrator', bypassRls: false); + + config(['database.connections.central' => array_merge(config('database.connections.pgsql'), [ + 'username' => $username, + 'password' => $password, + ])]); + + DB::reconnect(); + + // This table is owned by the newly created 'administrator' user + Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->string('name'); + + $table->string('tenant_id')->comment('rls'); + $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + + $table->timestamps(); + }); + + $tenant1 = Tenant::create(); + + // Create RLS policy for the orders table + pest()->artisan('tenants:rls'); + + $tenant1->run(fn () => Order::create(['name' => 'order1', 'tenant_id' => $tenant1->getTenantKey()])); + + // We are still using the 'administrator' user - owner of the orders table + + if ($forceRls) { + // RLS is forced, so by default, not even the table owner should be able to query the table protected by the RLS policy. + // The RLS policy is not being bypassed, 'unrecognized configuration parameter' means + // that the my.current_tenant session variable isn't set -- the RLS policy is *still* being enforced. + expect(fn () => Order::first())->toThrow(QueryException::class, 'unrecognized configuration parameter "my.current_tenant"'); + } else { + // RLS is not forced, so the table owner should be able to query the table, bypassing the RLS policy + expect(Order::first())->not()->toBeNull(); + } +})->with([true, false]); + +test('users with BYPASSRLS privilege can bypass RLS regardless of forceRls setting', function (bool $forceRls, bool $bypassRls) { + CreateUserWithRLSPolicies::$forceRls = $forceRls; + + // Drop all tables created in beforeEach + DB::statement("DROP TABLE authors, categories, posts, comments, reactions, articles;"); + + // Create a new user so we have control over his BYPASSRLS permission + // and use that as the new central connection user + [$username, $password] = createPostgresUser('administrator', 'password', $bypassRls); + + config(['database.connections.central' => array_merge(config('database.connections.pgsql'), [ + 'username' => $username, + 'password' => $password, + ])]); + + DB::reconnect(); + + Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->string('name'); + + $table->string('tenant_id')->comment('rls'); + $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + + $table->timestamps(); + }); + + $tenant1 = Tenant::create(); + + // Create RLS policy for the orders table + pest()->artisan('tenants:rls'); + + $tenant1->run(fn () => Order::create(['name' => 'order1', 'tenant_id' => $tenant1->getTenantKey()])); + + // We are still using the 'administrator' user + + if ($bypassRls) { + // Users with BYPASSRLS can always query tables regardless of forceRls setting + expect(Order::count())->toBe(1); + expect(Order::first()->name)->toBe('order1'); + } else { + // Users without BYPASSRLS are subject to RLS policies even if they're table owners when forceRls is true + // OR they can bypass as table owners (when forceRls=false) + if ($forceRls) { + // Even table owners need session variable -- this means RLS was NOT bypassed + expect(fn () => Order::first())->toThrow(QueryException::class, 'unrecognized configuration parameter "my.current_tenant"'); + } else { + // Table owners can bypass RLS automatically when forceRls is false + expect(Order::count())->toBe(1); + expect(Order::first()->name)->toBe('order1'); + } + } +})->with([true, false])->with([true, false]); + test('table rls manager generates queries correctly', function() { expect(array_values(app(TableRLSManager::class)->generateQueries()))->toEqualCanonicalizing([ << app(TableRLSManager::class)->generateTrees())->not()->toThrow(RecursiveRelationshipException::class); }); +function createPostgresUser(string $username, string $password = 'password', bool $bypassRls = false): array +{ + try { + DB::statement("DROP OWNED BY {$username};"); + } catch (\Throwable) {} + + DB::statement("DROP USER IF EXISTS {$username};"); + + DB::statement("CREATE USER {$username} WITH ENCRYPTED PASSWORD '{$password}'"); + DB::statement("ALTER USER {$username} CREATEDB"); + DB::statement("ALTER USER {$username} CREATEROLE"); + + // Grant BYPASSRLS privilege if requested + if ($bypassRls) { + DB::statement("ALTER USER {$username} BYPASSRLS"); + } + + // Grant privileges to the new central user + DB::statement("GRANT ALL PRIVILEGES ON DATABASE main to {$username}"); + DB::statement("GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO {$username}"); + DB::statement("GRANT ALL ON SCHEMA public TO {$username}"); + DB::statement("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO {$username}"); + DB::statement("GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO {$username}"); + + return [$username, $password]; +} + class Post extends Model { protected $guarded = []; @@ -715,3 +852,8 @@ class Author extends Model { protected $guarded = []; } + +class Order extends Model +{ + protected $guarded = []; +} diff --git a/tests/RLS/TraitManagerTest.php b/tests/RLS/TraitManagerTest.php index af2f6f84..b88b147b 100644 --- a/tests/RLS/TraitManagerTest.php +++ b/tests/RLS/TraitManagerTest.php @@ -28,6 +28,7 @@ use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel; use function Stancl\Tenancy\Tests\pest; beforeEach(function () { + CreateUserWithRLSPolicies::$forceRls = true; TraitRLSManager::$implicitRLS = true; TraitRLSManager::$modelDirectories = [__DIR__ . '/Etc']; TraitRLSManager::$excludedModels = [Article::class]; @@ -78,6 +79,10 @@ beforeEach(function () { }); }); +afterEach(function () { + CreateUserWithRLSPolicies::$forceRls = true; +}); + test('correct rls policies get created with the correct hash using trait manager', function () { $manager = app(TraitRLSManager::class); @@ -149,7 +154,8 @@ test('global scope is not applied when using rls with single db traits', functio expect(NonRLSComment::make()->hasGlobalScope(ParentModelScope::class))->toBeFalse(); }); -test('queries are correctly scoped using RLS with trait rls manager', function (bool $implicitRLS) { +test('queries are correctly scoped using RLS with trait rls manager', function (bool $implicitRLS, bool $forceRls) { + CreateUserWithRLSPolicies::$forceRls = $forceRls; TraitRLSManager::$implicitRLS = $implicitRLS; $postModel = $implicitRLS ? NonRLSPost::class : Post::class; @@ -263,10 +269,7 @@ test('queries are correctly scoped using RLS with trait rls manager', function ( expect(fn () => DB::statement("INSERT INTO comments (text, post_id) VALUES ('third comment', {$post1->id})")) ->toThrow(QueryException::class); -})->with([ - true, - false -]); +})->with([true, false])->with([true, false]); test('trait rls manager generates queries correctly', function() { /** @var TraitRLSManager $manager */ From e1fc0e107d236a113ab5e67fc1fa0044925100e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 18 Jun 2025 23:29:24 +0200 Subject: [PATCH 4/6] remove ignition dependencies --- composer.json | 2 - .../TenantCouldNotBeIdentifiedException.php | 42 +------------------ .../TenantColumnNotWhitelistedException.php | 5 +-- ...enantCouldNotBeIdentifiedByIdException.php | 5 +-- ...antCouldNotBeIdentifiedByPathException.php | 5 +-- ...dNotBeIdentifiedByRequestDataException.php | 5 +-- ...tCouldNotBeIdentifiedOnDomainException.php | 5 +-- 7 files changed, 7 insertions(+), 62 deletions(-) diff --git a/composer.json b/composer.json index e3a7faf4..3d3bd3eb 100644 --- a/composer.json +++ b/composer.json @@ -20,8 +20,6 @@ "ext-json": "*", "illuminate/support": "^12.0", "laravel/tinker": "^2.0", - "facade/ignition-contracts": "^1.0.2", - "spatie/ignition": "^1.4", "ramsey/uuid": "^4.7.3", "stancl/jobpipeline": "2.0.0-rc5", "stancl/virtualcolumn": "^1.5.0", diff --git a/src/Contracts/TenantCouldNotBeIdentifiedException.php b/src/Contracts/TenantCouldNotBeIdentifiedException.php index a83a0c75..b0ff74a4 100644 --- a/src/Contracts/TenantCouldNotBeIdentifiedException.php +++ b/src/Contracts/TenantCouldNotBeIdentifiedException.php @@ -5,49 +5,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Contracts; use Exception; -use Spatie\ErrorSolutions\Contracts\BaseSolution; -use Spatie\ErrorSolutions\Contracts\ProvidesSolution; -abstract class TenantCouldNotBeIdentifiedException extends Exception implements ProvidesSolution +abstract class TenantCouldNotBeIdentifiedException extends Exception { - /** Default solution title. */ - protected string $solutionTitle = 'Tenant could not be identified'; - - /** Default solution description. */ - protected string $solutionDescription = 'Are you sure this tenant exists?'; - - /** Set the message. */ - protected function tenantCouldNotBeIdentified(string $how): static + protected function tenantCouldNotBeIdentified(string $how): void { $this->message = 'Tenant could not be identified ' . $how; - - return $this; - } - - /** Set the solution title. */ - protected function title(string $solutionTitle): static - { - $this->solutionTitle = $solutionTitle; - - return $this; - } - - /** Set the solution description. */ - protected function description(string $solutionDescription): static - { - $this->solutionDescription = $solutionDescription; - - return $this; - } - - /** Get the Ignition description. */ - public function getSolution(): BaseSolution - { - return BaseSolution::create($this->solutionTitle) - ->setSolutionDescription($this->solutionDescription) - ->setDocumentationLinks([ - 'Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants', - 'Tenant Identification' => 'https://tenancyforlaravel.com/docs/v3/tenant-identification', - ]); } } diff --git a/src/Exceptions/TenantColumnNotWhitelistedException.php b/src/Exceptions/TenantColumnNotWhitelistedException.php index 6b59d8f7..20eb70b2 100644 --- a/src/Exceptions/TenantColumnNotWhitelistedException.php +++ b/src/Exceptions/TenantColumnNotWhitelistedException.php @@ -10,9 +10,6 @@ class TenantColumnNotWhitelistedException extends TenantCouldNotBeIdentifiedExce { public function __construct(int|string $tenant_id) { - $this - ->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id (column not whitelisted)") - ->title('Tenant could not be identified on this route because the used column is not whitelisted.') - ->description('Please add the column to the list of allowed columns in the PathTenantResolver config.'); + $this->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id (column not whitelisted)"); } } diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedByIdException.php b/src/Exceptions/TenantCouldNotBeIdentifiedByIdException.php index 6f61455e..36ef1d09 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedByIdException.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedByIdException.php @@ -10,9 +10,6 @@ class TenantCouldNotBeIdentifiedByIdException extends TenantCouldNotBeIdentified { public function __construct(int|string $tenant_id) { - $this - ->tenantCouldNotBeIdentified("by tenant key: $tenant_id") - ->title('Tenant could not be identified with that key') - ->description('Are you sure the key is correct and the tenant exists?'); + $this->tenantCouldNotBeIdentified("by tenant key: $tenant_id"); } } diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedByPathException.php b/src/Exceptions/TenantCouldNotBeIdentifiedByPathException.php index ef51f8bf..a004b39a 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedByPathException.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedByPathException.php @@ -10,9 +10,6 @@ class TenantCouldNotBeIdentifiedByPathException extends TenantCouldNotBeIdentifi { public function __construct(int|string $tenant_id) { - $this - ->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id") - ->title('Tenant could not be identified on this path') - ->description('Did you forget to create a tenant for this path?'); + $this->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id"); } } diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php b/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php index 1f1c98a1..ab2b92a9 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php @@ -10,9 +10,6 @@ class TenantCouldNotBeIdentifiedByRequestDataException extends TenantCouldNotBeI { public function __construct(mixed $payload) { - $this - ->tenantCouldNotBeIdentified("by request data with payload: $payload") - ->title('Tenant could not be identified using this request data') - ->description('Did you forget to create a tenant with this id?'); + $this->tenantCouldNotBeIdentified("by request data with payload: $payload"); } } diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php b/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php index 0421fe1b..5470c60a 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php @@ -10,9 +10,6 @@ class TenantCouldNotBeIdentifiedOnDomainException extends TenantCouldNotBeIdenti { public function __construct(string $domain) { - $this - ->tenantCouldNotBeIdentified("on domain $domain") - ->title('Tenant could not be identified on this domain') - ->description('Did you forget to create a tenant for this domain?'); + $this->tenantCouldNotBeIdentified("on domain $domain"); } } From 12fcbabd76315fdab6c33318354ec28b9bc8de9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 18 Jun 2025 23:52:35 +0200 Subject: [PATCH 5/6] phpstan fix --- src/helpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers.php b/src/helpers.php index 5794a000..903c5c40 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -30,7 +30,7 @@ if (! function_exists('tenant')) { return app(Tenant::class); } - return app(Tenant::class)?->getAttribute($key); + return app(Tenant::class)->getAttribute($key); } } From 748122906365f97f528897a2eec4d3d84a790388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 19 Jun 2025 00:12:38 +0200 Subject: [PATCH 6/6] revert regression in last commit, opt for a phpstan ignore instead --- src/helpers.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers.php b/src/helpers.php index 903c5c40..c8f5c9b3 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -30,7 +30,8 @@ if (! function_exists('tenant')) { return app(Tenant::class); } - return app(Tenant::class)->getAttribute($key); + // @phpstan-ignore-next-line nullsafe.neverNull + return app(Tenant::class)?->getAttribute($key); } }