From 6523f24a608593b5251c677ed54fd706e48c7928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Mon, 27 Oct 2025 17:54:39 +0100 Subject: [PATCH] Pending tenants: Add getPendingAttributes() This method lets the user specify default values for custom non-nullable columns. The primary use case is when the tenants table has a column like 'slug' and createPending() is called with no value for 'slug'. This would produce an exception due to the column having no default value. Here, getPendingAttributes() can set an initial dummy slug (like a randomly generated string) before it's overwritten during a pull. getPendingAttributes() accepts an $attributes array which corresponds to the attributes passed to createPending(). The array returned from getPendingAttributes() is ultimately merged with $attributes, so the user doesn't need to use the $attributes value in getPendingAttributes(), however it serves to provide more context when the pending attributes might be dependent on $attributes and therefore derived from the $attributes actually being used. Also fixed the `finally` branch in createPending() as it was potentially referencing the $tenant variable before it was initialized. --- src/Database/Concerns/HasPending.php | 17 ++++++++++++-- tests/Etc/Tenant.php | 7 ++++++ tests/PendingTenantsTest.php | 33 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/Database/Concerns/HasPending.php b/src/Database/Concerns/HasPending.php index 34a66544..0a572680 100644 --- a/src/Database/Concerns/HasPending.php +++ b/src/Database/Concerns/HasPending.php @@ -49,13 +49,15 @@ trait HasPending */ public static function createPending(array $attributes = []): Model&Tenant { + $tenant = null; + try { - $tenant = static::create($attributes); + $tenant = static::create(array_merge(static::getPendingAttributes($attributes), $attributes)); event(new CreatingPendingTenant($tenant)); } finally { // Update the pending_since value only after the tenant is created so it's // not marked as pending until after migrations, seeders, etc are run. - $tenant->update([ + $tenant?->update([ 'pending_since' => now()->timestamp, ]); } @@ -65,6 +67,17 @@ trait HasPending return $tenant; } + /** + * Attributes to be set when a pending tenant is initially created. + * + * @param array $attributes The attributes passed to createPending() (will be merged with the returned array) + * @return array + */ + public static function getPendingAttributes(array $attributes): array + { + return []; + } + /** * Pull a pending tenant from the pool or create a new one if the pool is empty. * diff --git a/tests/Etc/Tenant.php b/tests/Etc/Tenant.php index 731a179b..72570c50 100644 --- a/tests/Etc/Tenant.php +++ b/tests/Etc/Tenant.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests\Etc; +use Closure; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDomains; @@ -16,6 +17,7 @@ use Stancl\Tenancy\Database\Models; class Tenant extends Models\Tenant implements TenantWithDatabase { public static array $extraCustomColumns = []; + public static ?Closure $getPendingAttributesUsing = null; use HasDatabase, HasDomains, HasPending; @@ -23,4 +25,9 @@ class Tenant extends Models\Tenant implements TenantWithDatabase { return array_merge(parent::getCustomColumns(), static::$extraCustomColumns); } + + public static function getPendingAttributes(array $attributes): array + { + return static::$getPendingAttributesUsing ? (static::$getPendingAttributesUsing)($attributes) : []; + } } diff --git a/tests/PendingTenantsTest.php b/tests/PendingTenantsTest.php index 3339baaf..a90aceed 100644 --- a/tests/PendingTenantsTest.php +++ b/tests/PendingTenantsTest.php @@ -2,8 +2,12 @@ declare(strict_types=1); +use Illuminate\Database\QueryException; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Str; use Stancl\Tenancy\Commands\ClearPendingTenants; use Stancl\Tenancy\Commands\CreatePendingTenants; use Stancl\Tenancy\Events\CreatingPendingTenant; @@ -13,6 +17,13 @@ use Stancl\Tenancy\Events\PullingPendingTenant; use Stancl\Tenancy\Tests\Etc\Tenant; use function Stancl\Tenancy\Tests\pest; +beforeEach($cleanup = function () { + Tenant::$extraCustomColumns = []; + Tenant::$getPendingAttributesUsing = null; +}); + +afterEach($cleanup); + test('tenants are correctly identified as pending', function (){ Tenant::createPending(); @@ -191,3 +202,25 @@ test('commands run for pending tenants too if the with pending option is passed' $artisan->assertExitCode(0); }); + +test('pending tenants can have default attributes for non-nullable columns', function (bool $withPendingAttributes) { + Schema::table('tenants', function (Blueprint $table) { + $table->string('slug')->unique(); + }); + + Tenant::$extraCustomColumns = ['slug']; + if ($withPendingAttributes) Tenant::$getPendingAttributesUsing = fn () => [ + 'slug' => Str::random(8), + ]; + + $fn = fn () => Tenant::createPending(); + + // If there are non-nullable custom columns, and createPending() is called + // on its own without any values passed for those columns (as it would be called + // by the tenants:pending-create artisan command), we expect it to fail, unless + // getPendingAttributes() provides default values for those custom columns. + if ($withPendingAttributes) + expect($fn)->not()->toThrow(QueryException::class); + else + expect($fn)->toThrow(QueryException::class); +})->with([true, false]);