From 065e74c9bed8762ac27a941edc9e20db9cb45f12 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Mon, 17 Jan 2022 09:55:44 +0100 Subject: [PATCH 01/13] Add readied tenants Add config for readied tenants Add `create` and `clear` command Add Readied scope and static functions Add tests --- .DS_Store | Bin 0 -> 10244 bytes assets/config.php | 31 ++++++ src/Commands/ClearReadiedTenants.php | 67 +++++++++++++ src/Commands/CreateReadiedTenants.php | 62 ++++++++++++ src/Database/Concerns/ReadiedScope.php | 95 ++++++++++++++++++ src/Database/Concerns/WithReadied.php | 79 +++++++++++++++ src/Database/Models/Tenant.php | 3 +- src/Jobs/ClearReadiedTenants.php | 28 ++++++ src/Jobs/CreateReadiedTenants.php | 28 ++++++ src/TenancyServiceProvider.php | 2 + tests/Etc/Tenant.php | 3 +- tests/ReadiedTenantsTest.php | 130 +++++++++++++++++++++++++ 12 files changed, 526 insertions(+), 2 deletions(-) create mode 100644 .DS_Store create mode 100644 src/Commands/ClearReadiedTenants.php create mode 100644 src/Commands/CreateReadiedTenants.php create mode 100644 src/Database/Concerns/ReadiedScope.php create mode 100644 src/Database/Concerns/WithReadied.php create mode 100644 src/Jobs/ClearReadiedTenants.php create mode 100644 src/Jobs/CreateReadiedTenants.php create mode 100644 tests/ReadiedTenantsTest.php diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fc293b3611dd5a0057e1df2b9f49ff0acf6d4b1b GIT binary patch literal 10244 zcmZQzU|@7AO)+F(P+(wS;9!8z0z3>@0Z1N%F(jFwB5WY@7#IW?7>XDY8PXY2lgf(= zpmL+sXb6mkz-S1JhQMeDjE2DA3IRrlb2zvm^e8zR0;3@?8UpYT0F@7*4g!dFfVA-$ z8Xz=Ch=GxT0o(;(WME){X<`KT0~kPZAgv%8q!mPiv@$S)SYR{2S{WFjS{cFJ5Rg6r zuu+WQt_g?_*3Q5PwwZx}5o|L910zH`10&RCMraR(5u%-e5o|jH10zH`*vwI4Gz3ON z05t?aeRmFqWQKf(GKN&-{(DkRei8!%!?vV?oXp}91A}XfOw25-Z0sED9NZkS!5R7G z!6k_$rNvH(MbRK$NPd1!5{#Xg6qcD<9xou`oS#>cn35nUb26SQHbUnU|7Z z?v!7emr@MY3zgvDH_=frH?7rCsJ1jR&`~fmGOVrT|g+iOU`wC;_@mQx<=+U4z8YlL09+%1ci7+c*MfX zGK({la#BOfGgC@3A`^>~Q}a?X^U|X-Q&RIvGSf0si#P=tbvQ%v5_40p2uVmvNz2H} zqN|3P5>c9zSy-A`QYoO!!2z*DQC68#U@J#VN@7W>Z+;5M#1ycMtfH(s2S+qW+_@+< zu_P5D%%df%!zqx+5mKC4l?qnKqt9c=DNxKAlAW0cQ6p=@W5ywn!Wo*An3S3WRwQf5 zW6dGp%^6+>@i|0LgvXwPGa@-ZH#aq}q!=RRBv+V<=-7V-jOFV-8~;V?JXEV=ZGHV*_I&V;f^X;{?V@jFTB> zGA?3V%(#?s8RI&}?TkAZcQPJgJk5B9@f_oM#(Rv<89y?9V*Jebhw(4teoy%W(yyT2YBdUVv2~g%iS6Q|FNvU=#3$a;|V>)9dV-{mJ zV+ms!V+CU+V-;gFV=H4jV+Ug=<0Qr@j8hq>G0taP$heqs3FA`6b&MMrH!^Nw+|9U` zaX;e$#)IHge}(Zb<0Hn$kQ56~$G#9gjAZ [ + /** + * If disabled, readied tenants will be excluded from all tenant queries. Unless if + * told otherwise with ::withReadied() or ::onlyReadied(). + * Note: when disabled, this will also ignore tenants when runnings any tenants commands (migration, seed, etc.) + */ + 'include_in_scope' => true, + /** + * Defines how many tenants you want to be in a readied state. + * This value should be changed depending on how often a new tenant is created + * and how often you run the `tenancy:readied` command via the scheduler. + */ + 'count' => env('TENANCY_READIED_COUNT', 5), + + /** + * If needed, you can define a time limite after when an unused readied tenant + * will automatically be deleted. + * For this to work automatically, make sure to call the `tenancy:readied-clear` command in the scheduler. + * + * If both values are set to null, not time limit will be set and all readied tenants will be deleted. + */ + 'older_than_days' => env('TENANCY_READIED_OLDER_THAN_DAYS', null), + + 'older_than_hours' => env('TENANCY_READIED_OLDER_THAN_HOURS', null), + ], + /** * Database tenancy config. Used by DatabaseTenancyBootstrapper. */ diff --git a/src/Commands/ClearReadiedTenants.php b/src/Commands/ClearReadiedTenants.php new file mode 100644 index 00000000..05b0adcb --- /dev/null +++ b/src/Commands/ClearReadiedTenants.php @@ -0,0 +1,67 @@ +info('Cleaning readied tenants.'); + + $expireDate = now(); + // At the end, we will check if the value has been changed by comparing the two dates + $savedExpiredDate = $expireDate->copy()->toImmutable(); + + // If the all option is given, skip the expiry date configuration + if (! $this->option('all')) { + if ($olderThanDays = $this->option('older-days') ?? config('tenancy.readied.older_than_days')) { + $expireDate->subDays($olderThanDays); + } + + if ($olderThanHours = $this->option('older-hours') ?? config('tenancy.readied.older_than_hours')) { + $expireDate->subHours($olderThanHours); + } + } + + + $readiedTenantsDeletedCount = tenancy() + ->query() + ->onlyReadied() + ->when($savedExpiredDate->notEqualTo($expireDate), function (Builder $query) use ($expireDate) { + $query->where('data->readied', '<', $expireDate->timestamp); + }) + ->get() + ->each // This makes sure the events or triggered on the model + ->delete() + ->count(); + + $this->info("$readiedTenantsDeletedCount readied tenant(s) deleted."); + } +} diff --git a/src/Commands/CreateReadiedTenants.php b/src/Commands/CreateReadiedTenants.php new file mode 100644 index 00000000..90304198 --- /dev/null +++ b/src/Commands/CreateReadiedTenants.php @@ -0,0 +1,62 @@ +info('Deploying readied tenants.'); + + $readiedCountObjectif = (int)config('tenancy.readied.count'); + + $readiedTenantCount = $this->getReadiedTenantCount(); + + $deployedCount = 0; + while ($readiedTenantCount < $readiedCountObjectif) { + tenancy()->model()::createReadied(); + // We update the number of readied tenant every time with a query to get a live count. + // this prevents to deploy too many tenants if readied tenants have been deployed + // while this command is running. + $readiedTenantCount = $this->getReadiedTenantCount(); + $deployedCount++; + } + + $this->info("$deployedCount tenants deployed, $readiedCountObjectif tenant(s) are ready to be used."); + } + + /** + * Calculates the number of readied tenants currently deployed + * @return int + */ + private function getReadiedTenantCount(): int + { + return tenancy() + ->query() + ->onlyReadied() + ->count(); + } +} diff --git a/src/Database/Concerns/ReadiedScope.php b/src/Database/Concerns/ReadiedScope.php new file mode 100644 index 00000000..98b38106 --- /dev/null +++ b/src/Database/Concerns/ReadiedScope.php @@ -0,0 +1,95 @@ +when(!config('tenancy.readied.include_in_scope'), function (Builder $builder){ + $builder->whereNull('data->readied'); + }); + } + + /** + * Extend the query builder with the needed functions. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + public function extend(Builder $builder) + { + foreach ($this->extensions as $extension) { + $this->{"add{$extension}"}($builder); + } + } + /** + * Add the with-readied extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addWithReadied(Builder $builder) + { + $builder->macro('withReadied', function (Builder $builder, $withReadied = true) { + if (! $withReadied) { + return $builder->withoutReadied(); + } + + return $builder->withoutGlobalScope($this); + }); + } + + /** + * Add the without-readied extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addWithoutReadied(Builder $builder) + { + $builder->macro('withoutReadied', function (Builder $builder) { + + $builder->withoutGlobalScope($this)->whereNull('data->readied'); + + return $builder; + }); + } + + /** + * Add the only-readied extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addOnlyReadied(Builder $builder) + { + $builder->macro('onlyReadied', function (Builder $builder) { + + $builder->withoutGlobalScope($this)->whereNotNull('data->readied'); + + return $builder; + }); + } +} diff --git a/src/Database/Concerns/WithReadied.php b/src/Database/Concerns/WithReadied.php new file mode 100644 index 00000000..ead2dfe2 --- /dev/null +++ b/src/Database/Concerns/WithReadied.php @@ -0,0 +1,79 @@ +casts['readied'] = 'datetime'; + } + + + /** + * Determine if the model instance is in a readied state. + * + * @return bool + */ + public function readied() + { + return !is_null($this->readied); + } + + public static function createReadied($attributes = []): void + { + $tenant = static::create($attributes); + + // We add the readied value only after the model has then been created. + // this ensures the model is not marked as readied until the migrations, seeders, etc. are done + $tenant->update([ + 'readied' => now()->timestamp + ]); + } + + public static function pullReadiedTenant(bool $firstOrCreate = false): ?Tenant + { + if (!static::onlyReadied()->exists()) { + if (!$firstOrCreate) { + return null; + } + static::createReadied(); + } + + // At this point we can guarantee a readied tenant is free and can be called + $tenant = static::onlyReadied()->first(); + + $tenant->update([ + 'readied' => null + ]); + + return $tenant; + } +} diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index 4ec685b7..b575ff8f 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -26,7 +26,8 @@ class Tenant extends Model implements Contracts\Tenant Concerns\HasDataColumn, Concerns\HasInternalKeys, Concerns\TenantRun, - Concerns\InvalidatesResolverCache; + Concerns\InvalidatesResolverCache, + Concerns\WithReadied; protected $table = 'tenants'; protected $primaryKey = 'id'; diff --git a/src/Jobs/ClearReadiedTenants.php b/src/Jobs/ClearReadiedTenants.php new file mode 100644 index 00000000..6ca3d4ab --- /dev/null +++ b/src/Jobs/ClearReadiedTenants.php @@ -0,0 +1,28 @@ +publishes([ diff --git a/tests/Etc/Tenant.php b/tests/Etc/Tenant.php index 83840280..bc801f3b 100644 --- a/tests/Etc/Tenant.php +++ b/tests/Etc/Tenant.php @@ -7,9 +7,10 @@ namespace Stancl\Tenancy\Tests\Etc; use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDomains; +use Stancl\Tenancy\Database\Concerns\WithReadied; use Stancl\Tenancy\Database\Models; class Tenant extends Models\Tenant implements TenantWithDatabase { - use HasDatabase, HasDomains; + use HasDatabase, HasDomains, WithReadied; } diff --git a/tests/ReadiedTenantsTest.php b/tests/ReadiedTenantsTest.php new file mode 100644 index 00000000..70e439d4 --- /dev/null +++ b/tests/ReadiedTenantsTest.php @@ -0,0 +1,130 @@ +assertCount(1, Tenant::onlyReadied()->get()); + + Tenant::onlyReadied()->first()->update([ + 'readied' => null + ]); + + $this->assertCount(0, Tenant::onlyReadied()->get()); + } + + /** @test */ + public function readied_tenants_are_created_and_deleted_from_the_commands() + { + config(['tenancy.readied.count' => 4]); + + Artisan::call(CreateReadiedTenants::class); + + $this->assertCount(4, Tenant::onlyReadied()->get()); + + Artisan::call(ClearReadiedTenants::class); + + $this->assertCount(0, Tenant::onlyReadied()->get()); + } + + /** @test */ + public function clear_readied_tenants_command_only_delete_readied_tenants_older_than() + { + config(['tenancy.readied.count' => 2]); + + Artisan::call(CreateReadiedTenants::class); + + config(['tenancy.readied.older_than_days' => 4]); + + tenancy()->model()->query()->onlyReadied()->first()->update([ + 'readied' => now()->subDays() + ]); + + Artisan::call(ClearReadiedTenants::class); + + $this->assertCount(1, Tenant::onlyReadied()->get()); + } + + /** @test */ + public function clear_readied_tenants_command_all_option_overrides_config() + { + Tenant::createReadied(); + Tenant::createReadied(); + + tenancy()->model()->query()->onlyReadied()->first()->update([ + 'readied' => now()->subDays(10) + ]); + + config(['tenancy.readied.older_than_days' => 4]); + + Artisan::call(ClearReadiedTenants::class, [ + '--all' => true + ]); + + $this->assertCount(0, Tenant::onlyReadied()->get()); + } + + /** @test */ + public function tenancy_can_check_for_readied_tenants() + { + Tenant::query()->delete(); + + $this->assertFalse(Tenant::onlyReadied()->exists()); + + Tenant::createReadied(); + + $this->assertTrue(Tenant::onlyReadied()->exists()); + } + + /** @test */ + public function tenancy_can_pull_a_readied_tenant() + { + $this->assertNull(Tenant::pullReadiedTenant()); + + Tenant::createReadied(); + + $this->assertInstanceOf(Tenant::class, Tenant::pullReadiedTenant(true)); + } + + /** @test */ + public function tenancy_can_create_if_none_are_readied() + { + $this->assertDatabaseCount(Tenant::class, 0); + + Tenant::pullReadiedTenant(true); + + $this->assertDatabaseCount(Tenant::class, 1); + } + + /** @test */ + public function readied_tenants_global_scope_config_can_include_or_exclude() + { + Tenant::createReadied(); + + config(['tenancy.readied.include_in_scope' => false]); + + $this->assertCount(0, Tenant::all()); + + config(['tenancy.readied.include_in_scope' => true]); + + $this->assertCount(1, Tenant::all()); + Tenant::all(); + } +} From f487f92f0dc12928e614578be5c0c1a14de9fa78 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Tue, 18 Jan 2022 08:44:06 +0100 Subject: [PATCH 02/13] Fix initialize function name --- src/Database/Concerns/WithReadied.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Concerns/WithReadied.php b/src/Database/Concerns/WithReadied.php index ead2dfe2..96dc689c 100644 --- a/src/Database/Concerns/WithReadied.php +++ b/src/Database/Concerns/WithReadied.php @@ -31,7 +31,7 @@ trait WithReadied * * @return void */ - public function initializeSoftDeletes() + public function initializeWithReadied() { $this->casts['readied'] = 'datetime'; } From 30f0a2b134ea2a24d2e19932f4894d655c4a58e3 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Tue, 18 Jan 2022 18:19:40 +0100 Subject: [PATCH 03/13] Add readied events --- assets/TenancyServiceProvider.stub.php | 6 ++++++ src/Database/Concerns/WithReadied.php | 13 ++++++++++++- src/Events/PullingReadiedTenant.php | 9 +++++++++ src/Events/ReadiedTenantPulled.php | 9 +++++++++ src/Events/ReadyingTenant.php | 9 +++++++++ src/Events/TenantReadied.php | 9 +++++++++ tests/ReadiedTenantsTest.php | 26 ++++++++++++++++++++++++++ 7 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/Events/PullingReadiedTenant.php create mode 100644 src/Events/ReadiedTenantPulled.php create mode 100644 src/Events/ReadyingTenant.php create mode 100644 src/Events/TenantReadied.php diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 1d15f418..c2fcdd94 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -49,6 +49,12 @@ class TenancyServiceProvider extends ServiceProvider })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. ], + // Readied events + Events\ReadyingTenant::class => [], + Events\TenantReadied::class => [], + Events\PullingReadiedTenant::class => [], + Events\ReadiedTenantPulled::class => [], + // Domain events Events\CreatingDomain::class => [], Events\DomainCreated::class => [], diff --git a/src/Database/Concerns/WithReadied.php b/src/Database/Concerns/WithReadied.php index 96dc689c..d8397405 100644 --- a/src/Database/Concerns/WithReadied.php +++ b/src/Database/Concerns/WithReadied.php @@ -4,8 +4,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; -use Carbon\Carbon; use Stancl\Tenancy\Contracts\Tenant; +use Stancl\Tenancy\Events\PullingReadiedTenant; +use Stancl\Tenancy\Events\ReadiedTenantPulled; +use Stancl\Tenancy\Events\ReadyingTenant; +use Stancl\Tenancy\Events\TenantReadied; /** * @property null|Carbon $readied @@ -51,11 +54,15 @@ trait WithReadied { $tenant = static::create($attributes); + event(new ReadyingTenant($tenant)); + // We add the readied value only after the model has then been created. // this ensures the model is not marked as readied until the migrations, seeders, etc. are done $tenant->update([ 'readied' => now()->timestamp ]); + + event(new TenantReadied($tenant)); } public static function pullReadiedTenant(bool $firstOrCreate = false): ?Tenant @@ -70,10 +77,14 @@ trait WithReadied // At this point we can guarantee a readied tenant is free and can be called $tenant = static::onlyReadied()->first(); + event(new PullingReadiedTenant($tenant)); + $tenant->update([ 'readied' => null ]); + event(new ReadiedTenantPulled($tenant)); + return $tenant; } } diff --git a/src/Events/PullingReadiedTenant.php b/src/Events/PullingReadiedTenant.php new file mode 100644 index 00000000..fd71b21f --- /dev/null +++ b/src/Events/PullingReadiedTenant.php @@ -0,0 +1,9 @@ +assertCount(1, Tenant::all()); Tenant::all(); } + + /** @test */ + public function readied_events_are_triggerred() + { + Event::fake([ + ReadyingTenant::class, + TenantReadied::class, + PullingReadiedTenant::class, + ReadiedTenantPulled::class, + ]); + + Tenant::createReadied(); + + Event::assertDispatched(ReadyingTenant::class); + Event::assertDispatched(TenantReadied::class); + + Tenant::pullReadiedTenant(); + + Event::assertDispatched(PullingReadiedTenant::class); + Event::assertDispatched(ReadiedTenantPulled::class); + } } From 76a8d639ae4e3ed95eac1454ccfd85902f213293 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Tue, 18 Jan 2022 18:18:39 +0100 Subject: [PATCH 04/13] Fix readied column cast --- src/Database/Concerns/WithReadied.php | 4 ++-- tests/ReadiedTenantsTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Database/Concerns/WithReadied.php b/src/Database/Concerns/WithReadied.php index d8397405..2e7ccf3f 100644 --- a/src/Database/Concerns/WithReadied.php +++ b/src/Database/Concerns/WithReadied.php @@ -11,7 +11,7 @@ use Stancl\Tenancy\Events\ReadyingTenant; use Stancl\Tenancy\Events\TenantReadied; /** - * @property null|Carbon $readied + * @property $readied * * @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder withReadied(bool $withReadied = true) * @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder onlyReadied() @@ -36,7 +36,7 @@ trait WithReadied */ public function initializeWithReadied() { - $this->casts['readied'] = 'datetime'; + $this->casts['readied'] = 'timestamp'; } diff --git a/tests/ReadiedTenantsTest.php b/tests/ReadiedTenantsTest.php index 2abc7d08..9bee8209 100644 --- a/tests/ReadiedTenantsTest.php +++ b/tests/ReadiedTenantsTest.php @@ -56,10 +56,10 @@ class ReadiedTenantsTest extends TestCase Artisan::call(CreateReadiedTenants::class); - config(['tenancy.readied.older_than_days' => 4]); + config(['tenancy.readied.older_than_days' => 2]); tenancy()->model()->query()->onlyReadied()->first()->update([ - 'readied' => now()->subDays() + 'readied' => now()->subDays(5)->timestamp ]); Artisan::call(ClearReadiedTenants::class); From f8eef8d2ee5beb35e7b57e5da96a1baa17fa0d07 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Tue, 18 Jan 2022 18:39:12 +0100 Subject: [PATCH 05/13] Laravel 6 compatible --- tests/ReadiedTenantsTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ReadiedTenantsTest.php b/tests/ReadiedTenantsTest.php index 9bee8209..e78e322c 100644 --- a/tests/ReadiedTenantsTest.php +++ b/tests/ReadiedTenantsTest.php @@ -111,11 +111,11 @@ class ReadiedTenantsTest extends TestCase /** @test */ public function tenancy_can_create_if_none_are_readied() { - $this->assertDatabaseCount(Tenant::class, 0); + $this->assertCount(0, Tenant::all()); Tenant::pullReadiedTenant(true); - $this->assertDatabaseCount(Tenant::class, 1); + $this->assertCount(1, Tenant::all()); } /** @test */ From e11807490ee48b1d13942c5b8b4e6685c2a95576 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Wed, 19 Jan 2022 09:16:00 +0100 Subject: [PATCH 06/13] Add readied scope tests --- tests/ReadiedTenantsTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/ReadiedTenantsTest.php b/tests/ReadiedTenantsTest.php index e78e322c..3adfece0 100644 --- a/tests/ReadiedTenantsTest.php +++ b/tests/ReadiedTenantsTest.php @@ -35,6 +35,22 @@ class ReadiedTenantsTest extends TestCase $this->assertCount(0, Tenant::onlyReadied()->get()); } + /** @test */ + public function readied_trait_imports_query_scopes() + { + Tenant::createReadied(); + Tenant::create(); + Tenant::create(); + + $this->assertCount(1, Tenant::onlyReadied()->get()); + + $this->assertCount(3, Tenant::withReadied(true)->get()); + + $this->assertCount(2, Tenant::withReadied(false)->get()); + + $this->assertCount(2, Tenant::withoutReadied()->get()); + } + /** @test */ public function readied_tenants_are_created_and_deleted_from_the_commands() { From c8eadeb363a22e69ee30f38871c675c66a31031b Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Thu, 20 Jan 2022 08:24:54 +0100 Subject: [PATCH 07/13] Rename config from include_in_scope to include_in_queries --- assets/config.php | 2 +- src/Database/Concerns/ReadiedScope.php | 2 +- tests/ReadiedTenantsTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/config.php b/assets/config.php index d9e363bf..3b41f5a9 100644 --- a/assets/config.php +++ b/assets/config.php @@ -46,7 +46,7 @@ return [ * told otherwise with ::withReadied() or ::onlyReadied(). * Note: when disabled, this will also ignore tenants when runnings any tenants commands (migration, seed, etc.) */ - 'include_in_scope' => true, + 'include_in_queries' => true, /** * Defines how many tenants you want to be in a readied state. * This value should be changed depending on how often a new tenant is created diff --git a/src/Database/Concerns/ReadiedScope.php b/src/Database/Concerns/ReadiedScope.php index 98b38106..0062e367 100644 --- a/src/Database/Concerns/ReadiedScope.php +++ b/src/Database/Concerns/ReadiedScope.php @@ -27,7 +27,7 @@ class ReadiedScope implements Scope */ public function apply(Builder $builder, Model $model) { - $builder->when(!config('tenancy.readied.include_in_scope'), function (Builder $builder){ + $builder->when(!config('tenancy.readied.include_in_queries'), function (Builder $builder){ $builder->whereNull('data->readied'); }); } diff --git a/tests/ReadiedTenantsTest.php b/tests/ReadiedTenantsTest.php index 3adfece0..dcd2a336 100644 --- a/tests/ReadiedTenantsTest.php +++ b/tests/ReadiedTenantsTest.php @@ -139,11 +139,11 @@ class ReadiedTenantsTest extends TestCase { Tenant::createReadied(); - config(['tenancy.readied.include_in_scope' => false]); + config(['tenancy.readied.include_in_queries' => false]); $this->assertCount(0, Tenant::all()); - config(['tenancy.readied.include_in_scope' => true]); + config(['tenancy.readied.include_in_queries' => true]); $this->assertCount(1, Tenant::all()); Tenant::all(); From 500b2538fcf27b810aa96ce743aca6fa4cf12c09 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Wed, 9 Feb 2022 22:55:03 +0100 Subject: [PATCH 08/13] Change terminology to pending --- assets/TenancyServiceProvider.stub.php | 10 +- assets/config.php | 24 +-- ...iedTenants.php => ClearPendingTenants.php} | 26 +-- src/Commands/CreatePendingTenants.php | 64 +++++++ src/Commands/CreateReadiedTenants.php | 62 ------- src/Database/Concerns/HasPending.php | 90 +++++++++ .../{ReadiedScope.php => PendingScope.php} | 34 ++-- src/Database/Concerns/WithReadied.php | 90 --------- src/Database/Models/Tenant.php | 2 +- ...tReadied.php => CreatingPendingTenant.php} | 2 +- ...iedTenant.php => PendingTenantCreated.php} | 2 +- ...nantPulled.php => PendingTenantPulled.php} | 2 +- ...ingTenant.php => PullingPendingTenant.php} | 2 +- ...iedTenants.php => ClearPendingTenants.php} | 4 +- ...edTenants.php => CreatePendingTenants.php} | 4 +- src/TenancyServiceProvider.php | 4 +- tests/Etc/Tenant.php | 4 +- tests/Etc/tmp/queuetest.json | 2 +- tests/PendingTenantsTest.php | 172 ++++++++++++++++++ tests/ReadiedTenantsTest.php | 172 ------------------ 20 files changed, 387 insertions(+), 385 deletions(-) rename src/Commands/{ClearReadiedTenants.php => ClearPendingTenants.php} (66%) create mode 100644 src/Commands/CreatePendingTenants.php delete mode 100644 src/Commands/CreateReadiedTenants.php create mode 100644 src/Database/Concerns/HasPending.php rename src/Database/Concerns/{ReadiedScope.php => PendingScope.php} (63%) delete mode 100644 src/Database/Concerns/WithReadied.php rename src/Events/{TenantReadied.php => CreatingPendingTenant.php} (55%) rename src/Events/{PullingReadiedTenant.php => PendingTenantCreated.php} (55%) rename src/Events/{ReadiedTenantPulled.php => PendingTenantPulled.php} (55%) rename src/Events/{ReadyingTenant.php => PullingPendingTenant.php} (55%) rename src/Jobs/{ClearReadiedTenants.php => ClearPendingTenants.php} (81%) rename src/Jobs/{CreateReadiedTenants.php => CreatePendingTenants.php} (80%) create mode 100644 tests/PendingTenantsTest.php delete mode 100644 tests/ReadiedTenantsTest.php diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index c2fcdd94..9bc2cad0 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -49,11 +49,11 @@ class TenancyServiceProvider extends ServiceProvider })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. ], - // Readied events - Events\ReadyingTenant::class => [], - Events\TenantReadied::class => [], - Events\PullingReadiedTenant::class => [], - Events\ReadiedTenantPulled::class => [], + // Pending events + Events\CreatingPendingTenant::class => [], + Events\PendingTenantCreated::class => [], + Events\PullingPendingTenant::class => [], + Events\PendingTenantPulled::class => [], // Domain events Events\CreatingDomain::class => [], diff --git a/assets/config.php b/assets/config.php index 3b41f5a9..8f389710 100644 --- a/assets/config.php +++ b/assets/config.php @@ -37,33 +37,33 @@ return [ /** - * Readied tenancy config. + * Pending tenancy config. * This is useful if you're looking for a way to always have a tenant ready to be used. */ - 'readied' => [ + 'pending' => [ /** - * If disabled, readied tenants will be excluded from all tenant queries. Unless if - * told otherwise with ::withReadied() or ::onlyReadied(). + * If disabled, pending tenants will be excluded from all tenant queries. Unless if + * told otherwise with ::withPending() or ::onlyPending(). * Note: when disabled, this will also ignore tenants when runnings any tenants commands (migration, seed, etc.) */ 'include_in_queries' => true, /** - * Defines how many tenants you want to be in a readied state. + * Defines how many tenants you want to be in a pending state. * This value should be changed depending on how often a new tenant is created - * and how often you run the `tenancy:readied` command via the scheduler. + * and how often you run the `tenancy:pending` command via the scheduler. */ - 'count' => env('TENANCY_READIED_COUNT', 5), + 'count' => env('TENANCY_PENDING_COUNT', 5), /** - * If needed, you can define a time limite after when an unused readied tenant + * If needed, you can define a time limite after when an unused pending tenant * will automatically be deleted. - * For this to work automatically, make sure to call the `tenancy:readied-clear` command in the scheduler. + * For this to work automatically, make sure to call the `tenancy:pending-clear` command in the scheduler. * - * If both values are set to null, not time limit will be set and all readied tenants will be deleted. + * If both values are set to null, not time limit will be set and all pending tenants will be deleted. */ - 'older_than_days' => env('TENANCY_READIED_OLDER_THAN_DAYS', null), + 'older_than_days' => env('TENANCY_PENDING_OLDER_THAN_DAYS', null), - 'older_than_hours' => env('TENANCY_READIED_OLDER_THAN_HOURS', null), + 'older_than_hours' => env('TENANCY_PENDING_OLDER_THAN_HOURS', null), ], /** diff --git a/src/Commands/ClearReadiedTenants.php b/src/Commands/ClearPendingTenants.php similarity index 66% rename from src/Commands/ClearReadiedTenants.php rename to src/Commands/ClearPendingTenants.php index 05b0adcb..67d77d56 100644 --- a/src/Commands/ClearReadiedTenants.php +++ b/src/Commands/ClearPendingTenants.php @@ -7,24 +7,24 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Builder; -class ClearReadiedTenants extends Command +class ClearPendingTenants extends Command { /** * The name and signature of the console command. * * @var string */ - protected $signature = 'tenants:readied-clear - {--all : Override the default settings and deletes all readied tenants} - {--older-days= : Deletes all readied older than the amount of days} - {--older-hours= : Deletes all readied older than the amount of hours}'; + protected $signature = 'tenants:pending-clear + {--all : Override the default settings and deletes all pending tenants} + {--older-days= : Deletes all pending older than the amount of days} + {--older-hours= : Deletes all pending older than the amount of hours}'; /** * The console command description. * * @var string */ - protected $description = 'Removes any readied tenants'; + protected $description = 'Removes any pending tenants'; /** * Execute the console command. @@ -33,7 +33,7 @@ class ClearReadiedTenants extends Command */ public function handle() { - $this->info('Cleaning readied tenants.'); + $this->info('Cleaning pending tenants.'); $expireDate = now(); // At the end, we will check if the value has been changed by comparing the two dates @@ -41,27 +41,27 @@ class ClearReadiedTenants extends Command // If the all option is given, skip the expiry date configuration if (! $this->option('all')) { - if ($olderThanDays = $this->option('older-days') ?? config('tenancy.readied.older_than_days')) { + if ($olderThanDays = $this->option('older-days') ?? config('tenancy.pending.older_than_days')) { $expireDate->subDays($olderThanDays); } - if ($olderThanHours = $this->option('older-hours') ?? config('tenancy.readied.older_than_hours')) { + if ($olderThanHours = $this->option('older-hours') ?? config('tenancy.pending.older_than_hours')) { $expireDate->subHours($olderThanHours); } } - $readiedTenantsDeletedCount = tenancy() + $deletedPendingCount = tenancy() ->query() - ->onlyReadied() + ->onlyPending() ->when($savedExpiredDate->notEqualTo($expireDate), function (Builder $query) use ($expireDate) { - $query->where('data->readied', '<', $expireDate->timestamp); + $query->where('data->pending_since', '<', $expireDate->timestamp); }) ->get() ->each // This makes sure the events or triggered on the model ->delete() ->count(); - $this->info("$readiedTenantsDeletedCount readied tenant(s) deleted."); + $this->info("$deletedPendingCount pending tenant(s) deleted."); } } diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php new file mode 100644 index 00000000..45e15853 --- /dev/null +++ b/src/Commands/CreatePendingTenants.php @@ -0,0 +1,64 @@ +info('Deploying pendgin tenants.'); + + $pendingObjectifCount = (int)config('tenancy.pending.count'); + + $pendingCurrentCount = $this->getPendingTenantCount(); + + $deployedCount = 0; + while ($pendingCurrentCount < $pendingObjectifCount) { + tenancy()->model()::createPending(); + // We update the number of pending tenants every time with a query to get a live count. + // this prevents to deploy too many tenants if pending tenants are being created simultaneous somewhere else + // during the runtime of this command. + $pendingCurrentCount = $this->getPendingTenantCount(); + $deployedCount++; + } + + $this->info("$deployedCount tenants deployed, $pendingObjectifCount tenant(s) are ready to be used."); + + return self::SUCCESS; + } + + /** + * Calculates the number of pending tenants currently deployed + * @return int + */ + private function getPendingTenantCount(): int + { + return tenancy() + ->query() + ->onlyPending() + ->count(); + } +} diff --git a/src/Commands/CreateReadiedTenants.php b/src/Commands/CreateReadiedTenants.php deleted file mode 100644 index 90304198..00000000 --- a/src/Commands/CreateReadiedTenants.php +++ /dev/null @@ -1,62 +0,0 @@ -info('Deploying readied tenants.'); - - $readiedCountObjectif = (int)config('tenancy.readied.count'); - - $readiedTenantCount = $this->getReadiedTenantCount(); - - $deployedCount = 0; - while ($readiedTenantCount < $readiedCountObjectif) { - tenancy()->model()::createReadied(); - // We update the number of readied tenant every time with a query to get a live count. - // this prevents to deploy too many tenants if readied tenants have been deployed - // while this command is running. - $readiedTenantCount = $this->getReadiedTenantCount(); - $deployedCount++; - } - - $this->info("$deployedCount tenants deployed, $readiedCountObjectif tenant(s) are ready to be used."); - } - - /** - * Calculates the number of readied tenants currently deployed - * @return int - */ - private function getReadiedTenantCount(): int - { - return tenancy() - ->query() - ->onlyReadied() - ->count(); - } -} diff --git a/src/Database/Concerns/HasPending.php b/src/Database/Concerns/HasPending.php new file mode 100644 index 00000000..5187733e --- /dev/null +++ b/src/Database/Concerns/HasPending.php @@ -0,0 +1,90 @@ +casts['pending_since'] = 'timestamp'; + } + + + /** + * Determine if the model instance is in a pending state. + * + * @return bool + */ + public function pending() + { + return !is_null($this->pending_since); + } + + public static function createPending($attributes = []): void + { + $tenant = static::create($attributes); + + event(new CreatingPendingTenant($tenant)); + + // We add the pending value only after the model has then been created. + // this ensures the model is not marked as pending until the migrations, seeders, etc. are done + $tenant->update([ + 'pending_since' => now()->timestamp + ]); + + event(new PendingTenantCreated($tenant)); + } + + public static function pullPendingTenant(bool $firstOrCreate = false): ?Tenant + { + if (!static::onlyPending()->exists()) { + if (!$firstOrCreate) { + return null; + } + static::createPending(); + } + + // At this point we can guarantee a pending tenant is free and can be called. + $tenant = static::onlyPending()->first(); + + event(new PullingPendingTenant($tenant)); + + $tenant->update([ + 'pending_since' => null + ]); + + event(new PendingTenantPulled($tenant)); + + return $tenant; + } +} diff --git a/src/Database/Concerns/ReadiedScope.php b/src/Database/Concerns/PendingScope.php similarity index 63% rename from src/Database/Concerns/ReadiedScope.php rename to src/Database/Concerns/PendingScope.php index 0062e367..c5fad50c 100644 --- a/src/Database/Concerns/ReadiedScope.php +++ b/src/Database/Concerns/PendingScope.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; -class ReadiedScope implements Scope +class PendingScope implements Scope { /** @@ -16,7 +16,7 @@ class ReadiedScope implements Scope * * @var string[] */ - protected $extensions = ['WithReadied', 'WithoutReadied', 'OnlyReadied']; + protected $extensions = ['WithPending', 'WithoutPending', 'OnlyPending']; /** * Apply the scope to a given Eloquent query builder. @@ -27,8 +27,8 @@ class ReadiedScope implements Scope */ public function apply(Builder $builder, Model $model) { - $builder->when(!config('tenancy.readied.include_in_queries'), function (Builder $builder){ - $builder->whereNull('data->readied'); + $builder->when(!config('tenancy.pending.include_in_queries'), function (Builder $builder){ + $builder->whereNull('data->pending_since'); }); } @@ -45,16 +45,16 @@ class ReadiedScope implements Scope } } /** - * Add the with-readied extension to the builder. + * Add the with-pending extension to the builder. * * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ - protected function addWithReadied(Builder $builder) + protected function addWithPending(Builder $builder) { - $builder->macro('withReadied', function (Builder $builder, $withReadied = true) { - if (! $withReadied) { - return $builder->withoutReadied(); + $builder->macro('withPending', function (Builder $builder, $withPending = true) { + if (! $withPending) { + return $builder->withoutPending(); } return $builder->withoutGlobalScope($this); @@ -62,32 +62,32 @@ class ReadiedScope implements Scope } /** - * Add the without-readied extension to the builder. + * Add the without-pending extension to the builder. * * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ - protected function addWithoutReadied(Builder $builder) + protected function addWithoutPending(Builder $builder) { - $builder->macro('withoutReadied', function (Builder $builder) { + $builder->macro('withoutPending', function (Builder $builder) { - $builder->withoutGlobalScope($this)->whereNull('data->readied'); + $builder->withoutGlobalScope($this)->whereNull('data->pending_since'); return $builder; }); } /** - * Add the only-readied extension to the builder. + * Add the only-pending extension to the builder. * * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ - protected function addOnlyReadied(Builder $builder) + protected function addOnlyPending(Builder $builder) { - $builder->macro('onlyReadied', function (Builder $builder) { + $builder->macro('onlyPending', function (Builder $builder) { - $builder->withoutGlobalScope($this)->whereNotNull('data->readied'); + $builder->withoutGlobalScope($this)->whereNotNull('data->pending_since'); return $builder; }); diff --git a/src/Database/Concerns/WithReadied.php b/src/Database/Concerns/WithReadied.php deleted file mode 100644 index 2e7ccf3f..00000000 --- a/src/Database/Concerns/WithReadied.php +++ /dev/null @@ -1,90 +0,0 @@ -casts['readied'] = 'timestamp'; - } - - - /** - * Determine if the model instance is in a readied state. - * - * @return bool - */ - public function readied() - { - return !is_null($this->readied); - } - - public static function createReadied($attributes = []): void - { - $tenant = static::create($attributes); - - event(new ReadyingTenant($tenant)); - - // We add the readied value only after the model has then been created. - // this ensures the model is not marked as readied until the migrations, seeders, etc. are done - $tenant->update([ - 'readied' => now()->timestamp - ]); - - event(new TenantReadied($tenant)); - } - - public static function pullReadiedTenant(bool $firstOrCreate = false): ?Tenant - { - if (!static::onlyReadied()->exists()) { - if (!$firstOrCreate) { - return null; - } - static::createReadied(); - } - - // At this point we can guarantee a readied tenant is free and can be called - $tenant = static::onlyReadied()->first(); - - event(new PullingReadiedTenant($tenant)); - - $tenant->update([ - 'readied' => null - ]); - - event(new ReadiedTenantPulled($tenant)); - - return $tenant; - } -} diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index b575ff8f..625ec71c 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -27,7 +27,7 @@ class Tenant extends Model implements Contracts\Tenant Concerns\HasInternalKeys, Concerns\TenantRun, Concerns\InvalidatesResolverCache, - Concerns\WithReadied; + Concerns\HasPending; protected $table = 'tenants'; protected $primaryKey = 'id'; diff --git a/src/Events/TenantReadied.php b/src/Events/CreatingPendingTenant.php similarity index 55% rename from src/Events/TenantReadied.php rename to src/Events/CreatingPendingTenant.php index 526d3514..dfbe6c70 100644 --- a/src/Events/TenantReadied.php +++ b/src/Events/CreatingPendingTenant.php @@ -4,6 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Events; -class TenantReadied extends Contracts\TenantEvent +class CreatingPendingTenant extends Contracts\TenantEvent { } diff --git a/src/Events/PullingReadiedTenant.php b/src/Events/PendingTenantCreated.php similarity index 55% rename from src/Events/PullingReadiedTenant.php rename to src/Events/PendingTenantCreated.php index fd71b21f..f75f1a8e 100644 --- a/src/Events/PullingReadiedTenant.php +++ b/src/Events/PendingTenantCreated.php @@ -4,6 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Events; -class PullingReadiedTenant extends Contracts\TenantEvent +class PendingTenantCreated extends Contracts\TenantEvent { } diff --git a/src/Events/ReadiedTenantPulled.php b/src/Events/PendingTenantPulled.php similarity index 55% rename from src/Events/ReadiedTenantPulled.php rename to src/Events/PendingTenantPulled.php index 5bced1e3..b60740f4 100644 --- a/src/Events/ReadiedTenantPulled.php +++ b/src/Events/PendingTenantPulled.php @@ -4,6 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Events; -class ReadiedTenantPulled extends Contracts\TenantEvent +class PendingTenantPulled extends Contracts\TenantEvent { } diff --git a/src/Events/ReadyingTenant.php b/src/Events/PullingPendingTenant.php similarity index 55% rename from src/Events/ReadyingTenant.php rename to src/Events/PullingPendingTenant.php index 3e10a05f..2997c107 100644 --- a/src/Events/ReadyingTenant.php +++ b/src/Events/PullingPendingTenant.php @@ -4,6 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Events; -class ReadyingTenant extends Contracts\TenantEvent +class PullingPendingTenant extends Contracts\TenantEvent { } diff --git a/src/Jobs/ClearReadiedTenants.php b/src/Jobs/ClearPendingTenants.php similarity index 81% rename from src/Jobs/ClearReadiedTenants.php rename to src/Jobs/ClearPendingTenants.php index 6ca3d4ab..31720904 100644 --- a/src/Jobs/ClearReadiedTenants.php +++ b/src/Jobs/ClearPendingTenants.php @@ -12,7 +12,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Artisan; use Stancl\Tenancy\Contracts\TenantWithDatabase; -class ClearReadiedTenants implements ShouldQueue +class ClearPendingTenants implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -23,6 +23,6 @@ class ClearReadiedTenants implements ShouldQueue */ public function handle() { - Artisan::call('tenants:readied-clear'); + Artisan::call(\Stancl\Tenancy\Commands\ClearPendingTenants::class); } } diff --git a/src/Jobs/CreateReadiedTenants.php b/src/Jobs/CreatePendingTenants.php similarity index 80% rename from src/Jobs/CreateReadiedTenants.php rename to src/Jobs/CreatePendingTenants.php index d1db676e..bdd68e9a 100644 --- a/src/Jobs/CreateReadiedTenants.php +++ b/src/Jobs/CreatePendingTenants.php @@ -12,7 +12,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Artisan; use Stancl\Tenancy\Contracts\TenantWithDatabase; -class CreateReadiedTenants implements ShouldQueue +class CreatePendingTenants implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -23,6 +23,6 @@ class CreateReadiedTenants implements ShouldQueue */ public function handle() { - Artisan::call('tenants:readied'); + Artisan::call(\Stancl\Tenancy\Commands\CreatePendingTenants::class); } } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 69fb5165..a7e09b48 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -89,8 +89,8 @@ class TenancyServiceProvider extends ServiceProvider Commands\Rollback::class, Commands\TenantList::class, Commands\MigrateFresh::class, - Commands\CreateReadiedTenants::class, - Commands\ClearReadiedTenants::class, + Commands\CreatePendingTenants::class, + Commands\ClearPendingTenants::class, ]); $this->publishes([ diff --git a/tests/Etc/Tenant.php b/tests/Etc/Tenant.php index bc801f3b..8e817d97 100644 --- a/tests/Etc/Tenant.php +++ b/tests/Etc/Tenant.php @@ -7,10 +7,10 @@ namespace Stancl\Tenancy\Tests\Etc; use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDomains; -use Stancl\Tenancy\Database\Concerns\WithReadied; +use Stancl\Tenancy\Database\Concerns\HasPending; use Stancl\Tenancy\Database\Models; class Tenant extends Models\Tenant implements TenantWithDatabase { - use HasDatabase, HasDomains, WithReadied; + use HasDatabase, HasDomains, HasPending; } diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json index dc158c6b..00cf7c37 100644 --- a/tests/Etc/tmp/queuetest.json +++ b/tests/Etc/tmp/queuetest.json @@ -1 +1 @@ -{"userName":"Bar","shouldFail":false,"tenant_id":"The current tenant id is: a7f73c10-9879-40ae-b7b0-1ded7c1f7b1b"} \ No newline at end of file +{"tenant_id":"The current tenant id is: acme"} \ No newline at end of file diff --git a/tests/PendingTenantsTest.php b/tests/PendingTenantsTest.php new file mode 100644 index 00000000..ff26e9b9 --- /dev/null +++ b/tests/PendingTenantsTest.php @@ -0,0 +1,172 @@ +assertCount(1, Tenant::onlyPending()->get()); + + Tenant::onlyPending()->first()->update([ + 'pending_since' => null + ]); + + $this->assertCount(0, Tenant::onlyPending()->get()); + } + + /** @test */ + public function pending_trait_imports_query_scopes() + { + Tenant::createPending(); + Tenant::create(); + Tenant::create(); + + $this->assertCount(1, Tenant::onlyPending()->get()); + + $this->assertCount(3, Tenant::withPending(true)->get()); + + $this->assertCount(2, Tenant::withPending(false)->get()); + + $this->assertCount(2, Tenant::withoutPending()->get()); + } + + /** @test */ + public function pending_tenants_are_created_and_deleted_from_the_commands() + { + config(['tenancy.pending.count' => 4]); + + Artisan::call(CreatePendingTenants::class); + + $this->assertCount(4, Tenant::onlyPending()->get()); + + Artisan::call(ClearPendingTenants::class); + + $this->assertCount(0, Tenant::onlyPending()->get()); + } + + /** @test */ + public function clear_pending_tenants_command_only_delete_pending_tenants_older_than() + { + config(['tenancy.pending.count' => 2]); + + Artisan::call(CreatePendingTenants::class); + + config(['tenancy.pending.older_than_days' => 2]); + + tenancy()->model()->query()->onlyPending()->first()->update([ + 'pending_since' => now()->subDays(5)->timestamp + ]); + + Artisan::call(ClearPendingTenants::class); + + $this->assertCount(1, Tenant::onlyPending()->get()); + } + + /** @test */ + public function clear_pending_tenants_command_all_option_overrides_config() + { + Tenant::createPending(); + Tenant::createPending(); + + tenancy()->model()->query()->onlyPending()->first()->update([ + 'pending_since' => now()->subDays(10) + ]); + + config(['tenancy.pending.older_than_days' => 4]); + + Artisan::call(ClearPendingTenants::class, [ + '--all' => true + ]); + + $this->assertCount(0, Tenant::onlyPending()->get()); + } + + /** @test */ + public function tenancy_can_check_for_rpending_tenants() + { + Tenant::query()->delete(); + + $this->assertFalse(Tenant::onlyPending()->exists()); + + Tenant::createPending(); + + $this->assertTrue(Tenant::onlyPending()->exists()); + } + + /** @test */ + public function tenancy_can_pull_a_pending_tenant() + { + $this->assertNull(Tenant::pullPendingTenant()); + + Tenant::createPending(); + + $this->assertInstanceOf(Tenant::class, Tenant::pullPendingTenant(true)); + } + + /** @test */ + public function tenancy_can_create_if_none_are_pending() + { + $this->assertCount(0, Tenant::all()); + + Tenant::pullPendingTenant(true); + + $this->assertCount(1, Tenant::all()); + } + + /** @test */ + public function pending_tenants_global_scope_config_can_include_or_exclude() + { + Tenant::createPending(); + + config(['tenancy.pending.include_in_queries' => false]); + + $this->assertCount(0, Tenant::all()); + + config(['tenancy.pending.include_in_queries' => true]); + + $this->assertCount(1, Tenant::all()); + Tenant::all(); + } + + /** @test */ + public function pending_events_are_triggerred() + { + Event::fake([ + CreatingPendingTenant::class, + PendingTenantCreated::class, + PullingPendingTenant::class, + PendingTenantPulled::class, + ]); + + Tenant::createPending(); + + Event::assertDispatched(CreatingPendingTenant::class); + Event::assertDispatched(PendingTenantCreated::class); + + Tenant::pullPendingTenant(); + + Event::assertDispatched(PullingPendingTenant::class); + Event::assertDispatched(PendingTenantPulled::class); + } +} diff --git a/tests/ReadiedTenantsTest.php b/tests/ReadiedTenantsTest.php deleted file mode 100644 index dcd2a336..00000000 --- a/tests/ReadiedTenantsTest.php +++ /dev/null @@ -1,172 +0,0 @@ -assertCount(1, Tenant::onlyReadied()->get()); - - Tenant::onlyReadied()->first()->update([ - 'readied' => null - ]); - - $this->assertCount(0, Tenant::onlyReadied()->get()); - } - - /** @test */ - public function readied_trait_imports_query_scopes() - { - Tenant::createReadied(); - Tenant::create(); - Tenant::create(); - - $this->assertCount(1, Tenant::onlyReadied()->get()); - - $this->assertCount(3, Tenant::withReadied(true)->get()); - - $this->assertCount(2, Tenant::withReadied(false)->get()); - - $this->assertCount(2, Tenant::withoutReadied()->get()); - } - - /** @test */ - public function readied_tenants_are_created_and_deleted_from_the_commands() - { - config(['tenancy.readied.count' => 4]); - - Artisan::call(CreateReadiedTenants::class); - - $this->assertCount(4, Tenant::onlyReadied()->get()); - - Artisan::call(ClearReadiedTenants::class); - - $this->assertCount(0, Tenant::onlyReadied()->get()); - } - - /** @test */ - public function clear_readied_tenants_command_only_delete_readied_tenants_older_than() - { - config(['tenancy.readied.count' => 2]); - - Artisan::call(CreateReadiedTenants::class); - - config(['tenancy.readied.older_than_days' => 2]); - - tenancy()->model()->query()->onlyReadied()->first()->update([ - 'readied' => now()->subDays(5)->timestamp - ]); - - Artisan::call(ClearReadiedTenants::class); - - $this->assertCount(1, Tenant::onlyReadied()->get()); - } - - /** @test */ - public function clear_readied_tenants_command_all_option_overrides_config() - { - Tenant::createReadied(); - Tenant::createReadied(); - - tenancy()->model()->query()->onlyReadied()->first()->update([ - 'readied' => now()->subDays(10) - ]); - - config(['tenancy.readied.older_than_days' => 4]); - - Artisan::call(ClearReadiedTenants::class, [ - '--all' => true - ]); - - $this->assertCount(0, Tenant::onlyReadied()->get()); - } - - /** @test */ - public function tenancy_can_check_for_readied_tenants() - { - Tenant::query()->delete(); - - $this->assertFalse(Tenant::onlyReadied()->exists()); - - Tenant::createReadied(); - - $this->assertTrue(Tenant::onlyReadied()->exists()); - } - - /** @test */ - public function tenancy_can_pull_a_readied_tenant() - { - $this->assertNull(Tenant::pullReadiedTenant()); - - Tenant::createReadied(); - - $this->assertInstanceOf(Tenant::class, Tenant::pullReadiedTenant(true)); - } - - /** @test */ - public function tenancy_can_create_if_none_are_readied() - { - $this->assertCount(0, Tenant::all()); - - Tenant::pullReadiedTenant(true); - - $this->assertCount(1, Tenant::all()); - } - - /** @test */ - public function readied_tenants_global_scope_config_can_include_or_exclude() - { - Tenant::createReadied(); - - config(['tenancy.readied.include_in_queries' => false]); - - $this->assertCount(0, Tenant::all()); - - config(['tenancy.readied.include_in_queries' => true]); - - $this->assertCount(1, Tenant::all()); - Tenant::all(); - } - - /** @test */ - public function readied_events_are_triggerred() - { - Event::fake([ - ReadyingTenant::class, - TenantReadied::class, - PullingReadiedTenant::class, - ReadiedTenantPulled::class, - ]); - - Tenant::createReadied(); - - Event::assertDispatched(ReadyingTenant::class); - Event::assertDispatched(TenantReadied::class); - - Tenant::pullReadiedTenant(); - - Event::assertDispatched(PullingReadiedTenant::class); - Event::assertDispatched(ReadiedTenantPulled::class); - } -} From 30b49a9d25b3e32d25263de2e14cb701f947094b Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Sat, 12 Feb 2022 11:56:18 +0100 Subject: [PATCH 09/13] Update CreatePendingTenants.php --- src/Commands/CreatePendingTenants.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php index 45e15853..a248860e 100644 --- a/src/Commands/CreatePendingTenants.php +++ b/src/Commands/CreatePendingTenants.php @@ -47,7 +47,7 @@ class CreatePendingTenants extends Command $this->info("$deployedCount tenants deployed, $pendingObjectifCount tenant(s) are ready to be used."); - return self::SUCCESS; + return 1; } /** From cd70b57b83eb83a8cd8fcf2f3d9ee8b0841831f6 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Sat, 12 Feb 2022 13:18:35 +0100 Subject: [PATCH 10/13] Laravel 6 compatible --- src/Database/Concerns/PendingScope.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Database/Concerns/PendingScope.php b/src/Database/Concerns/PendingScope.php index c5fad50c..aaa48bfe 100644 --- a/src/Database/Concerns/PendingScope.php +++ b/src/Database/Concerns/PendingScope.php @@ -71,7 +71,11 @@ class PendingScope implements Scope { $builder->macro('withoutPending', function (Builder $builder) { - $builder->withoutGlobalScope($this)->whereNull('data->pending_since'); + // Only use whereNull('data->pending_since') when Laravel 6 support is dropped + // Issue fixed in Laravel 7 https://github.com/laravel/framework/pull/32417 + $builder->withoutGlobalScope($this) + ->where('data->pending_since', 'like', 'null') + ->orWhereNull('data'); return $builder; }); @@ -87,7 +91,9 @@ class PendingScope implements Scope { $builder->macro('onlyPending', function (Builder $builder) { - $builder->withoutGlobalScope($this)->whereNotNull('data->pending_since'); + // Use whereNotNull when Laravel 6 is dropped + // Issue fixed in Laravel 7 https://github.com/laravel/framework/pull/32417 + $builder->withoutGlobalScope($this)->where('data->pending_since', 'not like', 'null'); return $builder; }); From b73d047a39296c30810a8c7f47ebb2730e7625f4 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Sat, 12 Feb 2022 13:26:14 +0100 Subject: [PATCH 11/13] Update CreatePendingTenants.php --- src/Commands/CreatePendingTenants.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php index a248860e..76c461b1 100644 --- a/src/Commands/CreatePendingTenants.php +++ b/src/Commands/CreatePendingTenants.php @@ -13,7 +13,7 @@ class CreatePendingTenants extends Command * * @var string */ - protected $signature = 'tenants:pending {--count= The number of tenant to be in a pending state}'; + protected $signature = 'tenants:pending {--count= : The number of tenant to be in a pending state}'; /** * The console command description. @@ -29,9 +29,9 @@ class CreatePendingTenants extends Command */ public function handle() { - $this->info('Deploying pendgin tenants.'); + $this->info('Deploying pending tenants.'); - $pendingObjectifCount = (int)config('tenancy.pending.count'); + $pendingObjectifCount = (int) ($this->option('count') ?? config('tenancy.pending.count')); $pendingCurrentCount = $this->getPendingTenantCount(); From 0a37eb487add7a59b070abf7a9042f1b20b8281e Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Sat, 12 Feb 2022 15:30:50 +0100 Subject: [PATCH 12/13] runForMultiple can scope pending tenants --- src/Commands/Migrate.php | 2 +- src/Commands/MigrateFresh.php | 2 +- src/Commands/Rollback.php | 2 +- src/Commands/Run.php | 2 +- src/Commands/Seed.php | 2 +- src/Concerns/HasATenantsOption.php | 6 ++++++ src/Database/TenantCollection.php | 4 ++-- src/Tenancy.php | 14 ++++++++++---- tests/PendingTenantsTest.php | 24 ++++++++++++++++++++++++ 9 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index bf92dfcd..48d73a43 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -63,6 +63,6 @@ class Migrate extends MigrateCommand parent::handle(); event(new DatabaseMigrated($tenant)); - }); + }, $this->withPending()); } } diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index f50e2f5f..453bb304 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -45,7 +45,7 @@ final class MigrateFresh extends Command '--tenants' => [$tenant->getTenantKey()], '--force' => true, ]); - }); + }, $this->withPending()); $this->info('Done.'); } diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index 081872c8..f88df4f4 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -61,6 +61,6 @@ class Rollback extends RollbackCommand parent::handle(); event(new DatabaseRolledBack($tenant)); - }); + }, $this->withPending()); } } diff --git a/src/Commands/Run.php b/src/Commands/Run.php index aa518d7a..4e622c6b 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -52,6 +52,6 @@ class Run extends Command // Run command $this->call($this->argument('commandname'), array_merge($arguments, $options)); - }); + }, $this->withPending()); } } diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index dc97ae71..11cdaf48 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -59,6 +59,6 @@ class Seed extends SeedCommand parent::handle(); event(new DatabaseSeeded($tenant)); - }); + }, $this->withPending()); } } diff --git a/src/Concerns/HasATenantsOption.php b/src/Concerns/HasATenantsOption.php index a2b94ac5..055c6cd4 100644 --- a/src/Concerns/HasATenantsOption.php +++ b/src/Concerns/HasATenantsOption.php @@ -13,6 +13,7 @@ trait HasATenantsOption { return array_merge([ ['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null], + ['with-pending', null, InputOption::VALUE_NONE, 'include pending tenants in query', null], ], parent::getOptions()); } @@ -26,6 +27,11 @@ trait HasATenantsOption ->cursor(); } + protected function withPending(): bool + { + return $this->option('with-pending'); + } + public function __construct() { parent::__construct(); diff --git a/src/Database/TenantCollection.php b/src/Database/TenantCollection.php index ba3a8fab..c4d09784 100644 --- a/src/Database/TenantCollection.php +++ b/src/Database/TenantCollection.php @@ -16,9 +16,9 @@ use Stancl\Tenancy\Contracts\Tenant; */ class TenantCollection extends Collection { - public function runForEach(callable $callable): self + public function runForEach(callable $callable, bool $withPending = null): self { - tenancy()->runForMultiple($this->items, $callable); + tenancy()->runForMultiple($this->items, $callable, $withPending); return $this; } diff --git a/src/Tenancy.php b/src/Tenancy.php index 30f138e3..4051294f 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Traits\Macroable; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; +use Stancl\Tenancy\Database\Concerns\PendingScope; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedById; class Tenancy @@ -135,10 +136,15 @@ class Tenancy * @param callable $callback * @return void */ - public function runForMultiple($tenants, callable $callback) + public function runForMultiple($tenants, callable $callback, bool $withPending = null) { + $query = $this->model()->newQuery(); + + if (is_bool($withPending) && $this->model()::hasGlobalScope(PendingScope::class)){ + $query->withPending($withPending); + } // Convert null to all tenants - $tenants = is_null($tenants) ? $this->model()->cursor() : $tenants; + $tenants = is_null($tenants) ? $query->cursor() : $tenants; // Convert incrementing int ids to strings $tenants = is_int($tenants) ? (string) $tenants : $tenants; @@ -146,8 +152,8 @@ class Tenancy // Wrap string in array $tenants = is_string($tenants) ? [$tenants] : $tenants; - // Use all tenants if $tenants is falsey - $tenants = $tenants ?: $this->model()->cursor(); + // Use all tenants if $tenants is false + $tenants = $tenants ?: $query->cursor(); $originalTenant = $this->tenant; diff --git a/tests/PendingTenantsTest.php b/tests/PendingTenantsTest.php index ff26e9b9..9dc5c097 100644 --- a/tests/PendingTenantsTest.php +++ b/tests/PendingTenantsTest.php @@ -169,4 +169,28 @@ class PendingTenantsTest extends TestCase Event::assertDispatched(PullingPendingTenant::class); Event::assertDispatched(PendingTenantPulled::class); } + + /** @test */ + public function tenancy_run_for_multiple_can_scope_pending_tenants() + { + config(['tenancy.pending.include_in_queries' => false]); + + Tenant::createPending(); + Tenant::create(); + + $executedCount = 0; + tenancy()->runForMultiple([], function () use (&$executedCount){ + $executedCount++; + }, false); + + self::assertEquals(1, $executedCount); + + $executedCount = 0; + + tenancy()->runForMultiple([], function () use (&$executedCount){ + $executedCount++; + }, true); + + self::assertEquals(2, $executedCount); + } } From 7bb05873f0ea3cbb0c7418e3a3390a3f68136e15 Mon Sep 17 00:00:00 2001 From: "j.stein" Date: Sat, 12 Feb 2022 16:53:23 +0100 Subject: [PATCH 13/13] Fix issues --- src/Commands/Run.php | 3 ++- src/Concerns/HasATenantsOption.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 4e622c6b..f3b2197f 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -5,9 +5,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; +use Stancl\Tenancy\Concerns\HasATenantsOption; class Run extends Command { + use HasATenantsOption; /** * The console command description. * @@ -21,7 +23,6 @@ class Run extends Command * @var string */ protected $signature = "tenants:run {commandname : The command's name.} - {--tenants=* : The tenant(s) to run the command for. Default: all} {--argument=* : The arguments to pass to the command. Default: none} {--option=* : The options to pass to the command. Default: none}"; diff --git a/src/Concerns/HasATenantsOption.php b/src/Concerns/HasATenantsOption.php index 055c6cd4..120bb21c 100644 --- a/src/Concerns/HasATenantsOption.php +++ b/src/Concerns/HasATenantsOption.php @@ -27,9 +27,9 @@ trait HasATenantsOption ->cursor(); } - protected function withPending(): bool + protected function withPending(): ?bool { - return $this->option('with-pending'); + return $this->option('with-pending') ? true : null; } public function __construct()