mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 21:34:04 +00:00
* Add readied tenants Add config for readied tenants Add `create` and `clear` command Add Readied scope and static functions Add tests * Fix initialize function name * Add readied events * Fix readied column cast * Laravel 6 compatible * Add readied scope tests * Rename config from include_in_scope to include_in_queries * Change terminology to pending * Update CreatePendingTenants.php * Laravel 6 compatible * Update CreatePendingTenants.php * runForMultiple can scope pending tenants * Fix issues * Code and comment style improvements * Change 'tenant' to 'tenants' in command signature * Fix code style (php-cs-fixer) * Rename variables in CreatePendingTenants * Remove withPending from runForMultiple * Update tenants option trait * Update command that use tenants * Fix code style (php-cs-fixer) * Improve getTenants condition * Update config comments * Minor config comment corrections * Grammar fix * Update comments and naming * Correct comments * Improve writing * Remove pending tenant clearing time constraints * Allow using only one time constraint for clearing the pending tenants * phpunit to pest * Fix code style (php-cs-fixer) * Fix code style (php-cs-fixer) * [4.x] Optionally delete storage after tenant deletion (#938) * Add test for deleting storage after tenant deletion * Save `storage_path()` in a variable after initializing tenant in test Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Add DeleteTenantStorage listener * Update test name * Remove storage deletion config key * Remove tenant storage deletion events * Move tenant storage deletion to the DeletingTenant event Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * [4.x] Finish incomplete and missing tests (#947) * complete test sqlite manager customize path * complete test seed command works * complete uniqe exists test * Update SingleDatabaseTenancyTest.php * refactor the ternary into if condition * custom path * simplify if condition * random dir name * Update SingleDatabaseTenancyTest.php * Update CommandsTest.php * prefix random DB name with custom_ Co-authored-by: Samuel Štancl <samuel@archte.ch> * [4.x] Add batch tenancy queue bootstrapper (#874) * exclude master from CI * Add batch tenancy queue bootstrapper * add test case * skip tests for old versions * variable docblocks * use Laravel's connection getter and setter * convert test to pest * bottom space * singleton regis in TestCase * Update src/Bootstrappers/BatchTenancyBootstrapper.php Co-authored-by: Samuel Štancl <samuel@archte.ch> * convert batch class resolution to property level * enabled BatchTenancyBootstrapper by default * typehint DatabaseBatchRepository * refactore name * DI DB manager * typehint * Update config.php * use initialize() twice without end()ing tenancy to assert that previousConnection logic works correctly Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> Co-authored-by: Abrar Ahmad <abrar.dev99@gmail.com> Co-authored-by: Samuel Štancl <samuel@archte.ch> * [4.x] Storage::url() support (modified #689) (#909) * This adds support for tenancy aware Storage::url() method * Trigger CI build * Fixed Link command for Laravel v6, added StorageLink Events, more StorageLink tests, added RemoveStorageSymlinks Job, added Storage Jobs to TenancyServiceProvider stub, renamed misleading config example. * Fix typo * Fix code style (php-cs-fixer) * Update config comments * Format code in Link command, make writing more concise * Change "symLinks" to "symlinks" * Refactor Link command * Fix test name typo * Test fetching files using the public URL * Extract Link command logic into actions * Fix code style (php-cs-fixer) * Check if closure is null in CreateStorageSymlinksAction * Stop using command terminology in CreateStorageSymlinksAction * Separate the Storage::url() test cases * Update url_override comments * Remove afterLink closures, add types, move actions, add usage explanation to the symlink trait * Fix code style (php-cs-fixer) * Update public storage URL test * Fix issue with using str() * Improve url_override comment, add todos * add todo comment * fix docblock style * Add link command tests back * Add types to $tenants in the action handle() methods * Fix typo, update variable name formatting * Add tests for the symlink actions * Change possibleTenantSymlinks not to prefix the paths twice while tenancy is initialized * Fix code style (php-cs-fixer) * Stop testing storage directory existence in symlink test * Don't specify full namespace for Tenant model annotation * Don't specify full namespace in ActionTest * Remove "change to DI" todo * Remove possibleTenantSymlinks return annotation * Remove symlink-related jobs, instantiate and use actions * Revert "Remove symlink-related jobs, instantiate and use actions" This reverts commit547440c887. * Add a comment line about the possible tenant symlinks * Correct storagePath and publicPath variables * Revert "Correct storagePath and publicPath variables" This reverts commite3aa8e2086. * add a todo Co-authored-by: Martin Vlcek <martin@dontfreakout.eu> Co-authored-by: lukinovec <lukinovec@gmail.com> Co-authored-by: PHP CS Fixer <phpcsfixer@example.com> * Use HasTenantOptions in Link * Correct the tenant order in Run command * Fix code style (php-cs-fixer) * Fix formatting issue * Add missing imports * Fix code style (php-cs-fixer) * Use HasTenantOptions instead of the old trait name in Up/Down commands * Fix test name typo * Remove redundant passing of $withPending to runForMultiple in TenantCollection's runForEach * Make `with-pending` default to `config('tenancy.pending.include_in_queries')` in HasTenantOptions * Make `createPending()` return the created tenant * Fix code style (php-cs-fixer) * Remove tenant ordering * Fix code style (php-cs-fixer) * Remove duplicate tenancy bootstrappers config setting * Add and use getWithPendingOption method * Fix code style (php-cs-fixer) * Add optionNotPassedValue property * Test using --with-pending and the include_in_queries config value * Make with-pending VALUE_NONE * use plural in test names * fix test names * add pullPendingTenantFromPool * Add docblock type * Import commands * Fix code style (php-cs-fixer) * Move pending tenant tests to a more appropriate file * Delete queuetest from gitignore * Delete queuetest file * Add queuetest to gitignore * Rename pullPendingTenant to pullPending and don't pass bool to that method * Add a test that checks if pulling a pending tenant removes it from the pool * bump stancl/virtualcolumn to ^1.3 * Update pending tenant pulling test * Dynamically get columns for pending queries * Dynamically get virtual column name in ClearPendingTenants * Fix ClearPendingTenants bug * Make test name more accurate * Update test name * add a todo * Update include in queries test name * Remove `Tenant::query()->delete()` from pending tenant check test * Rename the pending tenant check test name * Update HasPending.php * fix all() call * code style * all() -> get() * Remove redundant `Tenant::all()` call Co-authored-by: j.stein <joristein@gmail.com> Co-authored-by: lukinovec <lukinovec@gmail.com> Co-authored-by: PHP CS Fixer <phpcsfixer@example.com> Co-authored-by: Abrar Ahmad <abrar.dev99@gmail.com> Co-authored-by: Riley19280 <rileyaven88@gmail.com> Co-authored-by: Martin Vlcek <martin@dontfreakout.eu>
This commit is contained in:
parent
bf504f4c79
commit
198f34f5e1
30 changed files with 693 additions and 29 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
.env
|
.env
|
||||||
|
.DS_Store
|
||||||
composer.lock
|
composer.lock
|
||||||
vendor/
|
vendor/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,12 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
Events\TenantMaintenanceModeEnabled::class => [],
|
Events\TenantMaintenanceModeEnabled::class => [],
|
||||||
Events\TenantMaintenanceModeDisabled::class => [],
|
Events\TenantMaintenanceModeDisabled::class => [],
|
||||||
|
|
||||||
|
// Pending tenant events
|
||||||
|
Events\CreatingPendingTenant::class => [],
|
||||||
|
Events\PendingTenantCreated::class => [],
|
||||||
|
Events\PullingPendingTenant::class => [],
|
||||||
|
Events\PendingTenantPulled::class => [],
|
||||||
|
|
||||||
// Domain events
|
// Domain events
|
||||||
Events\CreatingDomain::class => [],
|
Events\CreatingDomain::class => [],
|
||||||
Events\DomainCreated::class => [],
|
Events\DomainCreated::class => [],
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,25 @@ return [
|
||||||
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending tenants config.
|
||||||
|
* This is useful if you're looking for a way to always have a tenant ready to be used.
|
||||||
|
*/
|
||||||
|
'pending' => [
|
||||||
|
/**
|
||||||
|
* If disabled, pending tenants will be excluded from all tenant queries.
|
||||||
|
* You can still use ::withPending(), ::withoutPending() and ::onlyPending() to include or exclude the pending tenants regardless of this setting.
|
||||||
|
* Note: when disabled, this will also ignore pending tenants when running the tenant commands (migration, seed, etc.)
|
||||||
|
*/
|
||||||
|
'include_in_queries' => true,
|
||||||
|
/**
|
||||||
|
* Defines how many pending tenants you want to have ready in the pending tenant pool.
|
||||||
|
* This depends on the volume of tenants you're creating.
|
||||||
|
*/
|
||||||
|
'count' => env('TENANCY_PENDING_COUNT', 5),
|
||||||
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database tenancy config. Used by DatabaseTenancyBootstrapper.
|
* Database tenancy config. Used by DatabaseTenancyBootstrapper.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"spatie/ignition": "^1.4",
|
"spatie/ignition": "^1.4",
|
||||||
"ramsey/uuid": "^4.0",
|
"ramsey/uuid": "^4.0",
|
||||||
"stancl/jobpipeline": "^1.0",
|
"stancl/jobpipeline": "^1.0",
|
||||||
"stancl/virtualcolumn": "^1.0"
|
"stancl/virtualcolumn": "^1.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/framework": "^9.0",
|
"laravel/framework": "^9.0",
|
||||||
|
|
|
||||||
74
src/Commands/ClearPendingTenants.php
Normal file
74
src/Commands/ClearPendingTenants.php
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class ClearPendingTenants extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'tenants:pending-clear
|
||||||
|
{--all : Override the default settings and deletes all pending tenants}
|
||||||
|
{--older-than-days= : Deletes all pending tenants older than the amount of days}
|
||||||
|
{--older-than-hours= : Deletes all pending tenants older than the amount of hours}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Remove pending tenants.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Removing pending tenants.');
|
||||||
|
|
||||||
|
$expirationDate = now();
|
||||||
|
// We compare the original expiration date to the new one to check if the new one is different later
|
||||||
|
$originalExpirationDate = $expirationDate->copy()->toImmutable();
|
||||||
|
|
||||||
|
// Skip the time constraints if the 'all' option is given
|
||||||
|
if (! $this->option('all')) {
|
||||||
|
$olderThanDays = $this->option('older-than-days');
|
||||||
|
$olderThanHours = $this->option('older-than-hours');
|
||||||
|
|
||||||
|
if ($olderThanDays && $olderThanHours) {
|
||||||
|
$this->line("<options=bold,reverse;fg=red> Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components
|
||||||
|
$this->line('Please, choose only one of these options.');
|
||||||
|
|
||||||
|
return 1; // Exit code for failure
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($olderThanDays) {
|
||||||
|
$expirationDate->subDays($olderThanDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($olderThanHours) {
|
||||||
|
$expirationDate->subHours($olderThanHours);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$deletedTenantCount = tenancy()
|
||||||
|
->query()
|
||||||
|
->onlyPending()
|
||||||
|
->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) {
|
||||||
|
$query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp);
|
||||||
|
})
|
||||||
|
->get()
|
||||||
|
->each // Trigger the model events by deleting the tenants one by one
|
||||||
|
->delete()
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.');
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/Commands/CreatePendingTenants.php
Normal file
62
src/Commands/CreatePendingTenants.php
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CreatePendingTenants extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'tenants:pending-create {--count= : The number of pending tenants to be created}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Create pending tenants.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Creating pending tenants.');
|
||||||
|
|
||||||
|
$maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count'));
|
||||||
|
$pendingTenantCount = $this->getPendingTenantCount();
|
||||||
|
$createdCount = 0;
|
||||||
|
|
||||||
|
while ($pendingTenantCount < $maxPendingTenantCount) {
|
||||||
|
tenancy()->model()::createPending();
|
||||||
|
|
||||||
|
// Fetching the pending tenant count in each iteration prevents creating too many tenants
|
||||||
|
// If pending tenants are being created somewhere else while running this command
|
||||||
|
$pendingTenantCount = $this->getPendingTenantCount();
|
||||||
|
|
||||||
|
$createdCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.');
|
||||||
|
$this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the number of currently available pending tenants.
|
||||||
|
*/
|
||||||
|
private function getPendingTenantCount(): int
|
||||||
|
{
|
||||||
|
return tenancy()
|
||||||
|
->query()
|
||||||
|
->onlyPending()
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Commands;
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Foundation\Console\DownCommand;
|
use Illuminate\Foundation\Console\DownCommand;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
|
|
||||||
class Down extends DownCommand
|
class Down extends DownCommand
|
||||||
{
|
{
|
||||||
use HasATenantsOption;
|
use HasTenantOptions;
|
||||||
|
|
||||||
protected $signature = 'tenants:down
|
protected $signature = 'tenants:down
|
||||||
{--redirect= : The path that users should be redirected to}
|
{--redirect= : The path that users should be redirected to}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\LazyCollection;
|
use Illuminate\Support\LazyCollection;
|
||||||
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
|
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
|
||||||
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
|
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
|
|
||||||
class Link extends Command
|
class Link extends Command
|
||||||
{
|
{
|
||||||
use HasATenantsOption;
|
use HasTenantOptions;
|
||||||
|
|
||||||
protected $signature = 'tenants:link
|
protected $signature = 'tenants:link
|
||||||
{--tenants=* : The tenant(s) to run the command for. Default: all}
|
{--tenants=* : The tenant(s) to run the command for. Default: all}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,15 @@ namespace Stancl\Tenancy\Commands;
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
||||||
use Illuminate\Database\Migrations\Migrator;
|
use Illuminate\Database\Migrations\Migrator;
|
||||||
|
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||||
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Stancl\Tenancy\Events\DatabaseMigrated;
|
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||||
use Stancl\Tenancy\Events\MigratingDatabase;
|
use Stancl\Tenancy\Events\MigratingDatabase;
|
||||||
|
|
||||||
class Migrate extends MigrateCommand
|
class Migrate extends MigrateCommand
|
||||||
{
|
{
|
||||||
use HasATenantsOption, ExtendsLaravelCommand;
|
use HasTenantOptions, DealsWithMigrations, ExtendsLaravelCommand;
|
||||||
|
|
||||||
protected $description = 'Run migrations for tenant(s)';
|
protected $description = 'Run migrations for tenant(s)';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Commands;
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||||
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
class MigrateFresh extends Command
|
class MigrateFresh extends Command
|
||||||
{
|
{
|
||||||
use HasATenantsOption;
|
use HasTenantOptions, DealsWithMigrations;
|
||||||
|
|
||||||
protected $description = 'Drop all tables and re-run all migrations for tenant(s)';
|
protected $description = 'Drop all tables and re-run all migrations for tenant(s)';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,15 @@ namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Migrations\RollbackCommand;
|
use Illuminate\Database\Console\Migrations\RollbackCommand;
|
||||||
use Illuminate\Database\Migrations\Migrator;
|
use Illuminate\Database\Migrations\Migrator;
|
||||||
|
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||||
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Stancl\Tenancy\Events\DatabaseRolledBack;
|
use Stancl\Tenancy\Events\DatabaseRolledBack;
|
||||||
use Stancl\Tenancy\Events\RollingBackDatabase;
|
use Stancl\Tenancy\Events\RollingBackDatabase;
|
||||||
|
|
||||||
class Rollback extends RollbackCommand
|
class Rollback extends RollbackCommand
|
||||||
{
|
{
|
||||||
use HasATenantsOption, ExtendsLaravelCommand;
|
use HasTenantOptions, DealsWithMigrations, ExtendsLaravelCommand;
|
||||||
|
|
||||||
protected $description = 'Rollback migrations for tenant(s).';
|
protected $description = 'Rollback migrations for tenant(s).';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Contracts\Console\Kernel;
|
use Illuminate\Contracts\Console\Kernel;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Symfony\Component\Console\Input\ArgvInput;
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
|
||||||
class Run extends Command
|
class Run extends Command
|
||||||
{
|
{
|
||||||
use HasATenantsOption;
|
use HasTenantOptions;
|
||||||
|
|
||||||
protected $description = 'Run a command for tenant(s)';
|
protected $description = 'Run a command for tenant(s)';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Database\ConnectionResolverInterface;
|
use Illuminate\Database\ConnectionResolverInterface;
|
||||||
use Illuminate\Database\Console\Seeds\SeedCommand;
|
use Illuminate\Database\Console\Seeds\SeedCommand;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Stancl\Tenancy\Events\DatabaseSeeded;
|
use Stancl\Tenancy\Events\DatabaseSeeded;
|
||||||
use Stancl\Tenancy\Events\SeedingDatabase;
|
use Stancl\Tenancy\Events\SeedingDatabase;
|
||||||
|
|
||||||
class Seed extends SeedCommand
|
class Seed extends SeedCommand
|
||||||
{
|
{
|
||||||
use HasATenantsOption;
|
use HasTenantOptions;
|
||||||
|
|
||||||
protected $description = 'Seed tenant database(s).';
|
protected $description = 'Seed tenant database(s).';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Commands;
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
|
|
||||||
class Up extends Command
|
class Up extends Command
|
||||||
{
|
{
|
||||||
use HasATenantsOption;
|
use HasTenantOptions;
|
||||||
|
|
||||||
protected $signature = 'tenants:up';
|
protected $signature = 'tenants:up';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,19 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Concerns;
|
namespace Stancl\Tenancy\Concerns;
|
||||||
|
|
||||||
use Illuminate\Support\LazyCollection;
|
use Illuminate\Support\LazyCollection;
|
||||||
|
use Stancl\Tenancy\Database\Concerns\PendingScope;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
trait HasATenantsOption
|
/**
|
||||||
|
* Adds 'tenants' and 'with-pending' options.
|
||||||
|
*/
|
||||||
|
trait HasTenantOptions
|
||||||
{
|
{
|
||||||
protected function getOptions()
|
protected function getOptions()
|
||||||
{
|
{
|
||||||
return array_merge([
|
return array_merge([
|
||||||
['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, '', null],
|
['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, '', null],
|
||||||
|
['with-pending', null, InputOption::VALUE_NONE, 'include pending tenants in query'],
|
||||||
], parent::getOptions());
|
], parent::getOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,6 +28,9 @@ trait HasATenantsOption
|
||||||
->when($this->option('tenants'), function ($query) {
|
->when($this->option('tenants'), function ($query) {
|
||||||
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
|
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
|
||||||
})
|
})
|
||||||
|
->when(tenancy()->model()::hasGlobalScope(PendingScope::class), function ($query) {
|
||||||
|
$query->withPending(config('tenancy.pending.include_in_queries') ?: $this->option('with-pending'));
|
||||||
|
})
|
||||||
->cursor();
|
->cursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
103
src/Database/Concerns/HasPending.php
Normal file
103
src/Database/Concerns/HasPending.php
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
use Stancl\Tenancy\Events\CreatingPendingTenant;
|
||||||
|
use Stancl\Tenancy\Events\PendingTenantCreated;
|
||||||
|
use Stancl\Tenancy\Events\PendingTenantPulled;
|
||||||
|
use Stancl\Tenancy\Events\PullingPendingTenant;
|
||||||
|
|
||||||
|
// todo consider adding a method that sets pending_since to null — to flag tenants as not-pending
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Carbon $pending_since
|
||||||
|
*
|
||||||
|
* @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder withPending(bool $withPending = true)
|
||||||
|
* @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder onlyPending()
|
||||||
|
* @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder withoutPending()
|
||||||
|
*/
|
||||||
|
trait HasPending
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Boot the has pending trait for a model.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function bootHasPending()
|
||||||
|
{
|
||||||
|
static::addGlobalScope(new PendingScope());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the has pending trait for an instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function initializeHasPending()
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a pending tenant. */
|
||||||
|
public static function createPending($attributes = []): Tenant
|
||||||
|
{
|
||||||
|
$tenant = static::create($attributes);
|
||||||
|
|
||||||
|
event(new CreatingPendingTenant($tenant));
|
||||||
|
|
||||||
|
// Update the pending_since value only after the tenant is created so it's
|
||||||
|
// Not marked as pending until finishing running the migrations, seeders, etc.
|
||||||
|
$tenant->update([
|
||||||
|
'pending_since' => now()->timestamp,
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new PendingTenantCreated($tenant));
|
||||||
|
|
||||||
|
return $tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pull a pending tenant. */
|
||||||
|
public static function pullPending(): Tenant
|
||||||
|
{
|
||||||
|
return static::pullPendingFromPool(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try to pull a tenant from the pool of pending tenants. */
|
||||||
|
public static function pullPendingFromPool(bool $firstOrCreate = false): ?Tenant
|
||||||
|
{
|
||||||
|
if (! static::onlyPending()->exists()) {
|
||||||
|
if (! $firstOrCreate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static::createPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pending tenant is surely available at this point
|
||||||
|
$tenant = static::onlyPending()->first();
|
||||||
|
|
||||||
|
event(new PullingPendingTenant($tenant));
|
||||||
|
|
||||||
|
$tenant->update([
|
||||||
|
'pending_since' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new PendingTenantPulled($tenant));
|
||||||
|
|
||||||
|
return $tenant;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/Database/Concerns/PendingScope.php
Normal file
88
src/Database/Concerns/PendingScope.php
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Scope;
|
||||||
|
|
||||||
|
class PendingScope implements Scope
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* All of the extensions to be added to the builder.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $extensions = ['WithPending', 'WithoutPending', 'OnlyPending'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the scope to a given Eloquent query builder.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function apply(Builder $builder, Model $model)
|
||||||
|
{
|
||||||
|
$builder->when(! config('tenancy.pending.include_in_queries'), function (Builder $builder) {
|
||||||
|
$builder->whereNull($builder->getModel()->getColumnForQuery('pending_since'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the query builder with the needed functions.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function extend(Builder $builder)
|
||||||
|
{
|
||||||
|
foreach ($this->extensions as $extension) {
|
||||||
|
$this->{"add{$extension}"}($builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add the with-pending extension to the builder.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addWithPending(Builder $builder)
|
||||||
|
{
|
||||||
|
$builder->macro('withPending', function (Builder $builder, $withPending = true) {
|
||||||
|
if (! $withPending) {
|
||||||
|
return $builder->withoutPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder->withoutGlobalScope($this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the without-pending extension to the builder.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addWithoutPending(Builder $builder)
|
||||||
|
{
|
||||||
|
$builder->macro('withoutPending', function (Builder $builder) {
|
||||||
|
$builder->withoutGlobalScope($this)
|
||||||
|
->whereNull($builder->getModel()->getColumnForQuery('pending_since'))
|
||||||
|
->orWhereNull($builder->getModel()->getDataColumn());
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the only-pending extension to the builder.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addOnlyPending(Builder $builder)
|
||||||
|
{
|
||||||
|
$builder->macro('onlyPending', function (Builder $builder) {
|
||||||
|
$builder->withoutGlobalScope($this)->whereNotNull($builder->getModel()->getColumnForQuery('pending_since'));
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ class Tenant extends Model implements Contracts\Tenant
|
||||||
Concerns\GeneratesIds,
|
Concerns\GeneratesIds,
|
||||||
Concerns\HasInternalKeys,
|
Concerns\HasInternalKeys,
|
||||||
Concerns\TenantRun,
|
Concerns\TenantRun,
|
||||||
|
Concerns\HasPending,
|
||||||
Concerns\InitializationHelpers,
|
Concerns\InitializationHelpers,
|
||||||
Concerns\InvalidatesResolverCache;
|
Concerns\InvalidatesResolverCache;
|
||||||
|
|
||||||
|
|
|
||||||
9
src/Events/CreatingPendingTenant.php
Normal file
9
src/Events/CreatingPendingTenant.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class CreatingPendingTenant extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
9
src/Events/PendingTenantCreated.php
Normal file
9
src/Events/PendingTenantCreated.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class PendingTenantCreated extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
9
src/Events/PendingTenantPulled.php
Normal file
9
src/Events/PendingTenantPulled.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class PendingTenantPulled extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
9
src/Events/PullingPendingTenant.php
Normal file
9
src/Events/PullingPendingTenant.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class PullingPendingTenant extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
28
src/Jobs/ClearPendingTenants.php
Normal file
28
src/Jobs/ClearPendingTenants.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Jobs;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Stancl\Tenancy\Commands\ClearPendingTenants as ClearPendingTenantsCommand;
|
||||||
|
|
||||||
|
class ClearPendingTenants implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
Artisan::call(ClearPendingTenantsCommand::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Jobs/CreatePendingTenants.php
Normal file
28
src/Jobs/CreatePendingTenants.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Jobs;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Stancl\Tenancy\Commands\CreatePendingTenants as CreatePendingTenantsCommand;
|
||||||
|
|
||||||
|
class CreatePendingTenants implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
Artisan::call(CreatePendingTenantsCommand::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -156,7 +156,7 @@ class Tenancy
|
||||||
// Wrap string in array
|
// Wrap string in array
|
||||||
$tenants = is_string($tenants) ? [$tenants] : $tenants;
|
$tenants = is_string($tenants) ? [$tenants] : $tenants;
|
||||||
|
|
||||||
// Use all tenants if $tenants is falsey
|
// Use all tenants if $tenants is falsy
|
||||||
$tenants = $tenants ?: $this->model()->cursor(); // todo1 phpstan thinks this isn't needed, but tests fail without it
|
$tenants = $tenants ?: $this->model()->cursor(); // todo1 phpstan thinks this isn't needed, but tests fail without it
|
||||||
|
|
||||||
$originalTenant = $this->tenant;
|
$originalTenant = $this->tenant;
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,9 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
$this->commands([
|
$this->commands([
|
||||||
|
Commands\Up::class,
|
||||||
Commands\Run::class,
|
Commands\Run::class,
|
||||||
|
Commands\Down::class,
|
||||||
Commands\Link::class,
|
Commands\Link::class,
|
||||||
Commands\Seed::class,
|
Commands\Seed::class,
|
||||||
Commands\Install::class,
|
Commands\Install::class,
|
||||||
|
|
@ -87,8 +89,8 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
Commands\TenantList::class,
|
Commands\TenantList::class,
|
||||||
Commands\TenantDump::class,
|
Commands\TenantDump::class,
|
||||||
Commands\MigrateFresh::class,
|
Commands\MigrateFresh::class,
|
||||||
Commands\Down::class,
|
Commands\ClearPendingTenants::class,
|
||||||
Commands\Up::class,
|
Commands\CreatePendingTenants::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->app->extend(FreshCommand::class, function () {
|
$this->app->extend(FreshCommand::class, function () {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) {
|
if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) {
|
||||||
unlink($schemaPath);
|
unlink($schemaPath);
|
||||||
|
|
@ -34,10 +33,6 @@ beforeEach(function () {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->toListener());
|
})->toListener());
|
||||||
|
|
||||||
config(['tenancy.bootstrappers' => [
|
|
||||||
DatabaseTenancyBootstrapper::class,
|
|
||||||
]]);
|
|
||||||
|
|
||||||
config([
|
config([
|
||||||
'tenancy.bootstrappers' => [
|
'tenancy.bootstrappers' => [
|
||||||
DatabaseTenancyBootstrapper::class,
|
DatabaseTenancyBootstrapper::class,
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Tests\Etc\Console;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Stancl\Tenancy\Concerns\TenantAwareCommand;
|
use Stancl\Tenancy\Concerns\TenantAwareCommand;
|
||||||
use Stancl\Tenancy\Tests\Etc\User;
|
use Stancl\Tenancy\Tests\Etc\User;
|
||||||
|
|
||||||
class AddUserCommand extends Command
|
class AddUserCommand extends Command
|
||||||
{
|
{
|
||||||
use TenantAwareCommand, HasATenantsOption;
|
use TenantAwareCommand, HasTenantOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name and signature of the console command.
|
* The name and signature of the console command.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Tests\Etc;
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
use Stancl\Tenancy\Database\Concerns\HasDatabase;
|
use Stancl\Tenancy\Database\Concerns\HasDatabase;
|
||||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||||
|
use Stancl\Tenancy\Database\Concerns\HasPending;
|
||||||
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
||||||
use Stancl\Tenancy\Database\Models;
|
use Stancl\Tenancy\Database\Models;
|
||||||
|
|
||||||
|
|
@ -15,5 +16,5 @@ use Stancl\Tenancy\Database\Models;
|
||||||
*/
|
*/
|
||||||
class Tenant extends Models\Tenant implements TenantWithDatabase
|
class Tenant extends Models\Tenant implements TenantWithDatabase
|
||||||
{
|
{
|
||||||
use HasDatabase, HasDomains, MaintenanceMode;
|
use HasDatabase, HasDomains, HasPending, MaintenanceMode;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
209
tests/PendingTenantsTest.php
Normal file
209
tests/PendingTenantsTest.php
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Stancl\Tenancy\Commands\ClearPendingTenants;
|
||||||
|
use Stancl\Tenancy\Commands\CreatePendingTenants;
|
||||||
|
use Stancl\Tenancy\Events\CreatingPendingTenant;
|
||||||
|
use Stancl\Tenancy\Events\PendingTenantCreated;
|
||||||
|
use Stancl\Tenancy\Events\PendingTenantPulled;
|
||||||
|
use Stancl\Tenancy\Events\PullingPendingTenant;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
test('tenants are correctly identified as pending', function (){
|
||||||
|
Tenant::createPending();
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(1);
|
||||||
|
|
||||||
|
Tenant::onlyPending()->first()->update([
|
||||||
|
'pending_since' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pending trait adds query scopes', function () {
|
||||||
|
Tenant::createPending();
|
||||||
|
Tenant::create();
|
||||||
|
Tenant::create();
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(1)
|
||||||
|
->and(Tenant::withPending(true)->count())->toBe(3)
|
||||||
|
->and(Tenant::withPending(false)->count())->toBe(2)
|
||||||
|
->and(Tenant::withoutPending()->count())->toBe(2);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pending tenants can be created and deleted using commands', function () {
|
||||||
|
config(['tenancy.pending.count' => 4]);
|
||||||
|
|
||||||
|
Artisan::call(CreatePendingTenants::class);
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(4);
|
||||||
|
|
||||||
|
Artisan::call(ClearPendingTenants::class);
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CreatePendingTenants command can have an older than constraint', function () {
|
||||||
|
config(['tenancy.pending.count' => 2]);
|
||||||
|
|
||||||
|
Artisan::call(CreatePendingTenants::class);
|
||||||
|
|
||||||
|
tenancy()->model()->query()->onlyPending()->first()->update([
|
||||||
|
'pending_since' => now()->subDays(5)->timestamp
|
||||||
|
]);
|
||||||
|
|
||||||
|
Artisan::call('tenants:pending-clear --older-than-days=2');
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CreatePendingTenants command cannot run with both time constraints', function () {
|
||||||
|
pest()->artisan('tenants:pending-clear --older-than-days=2 --older-than-hours=2')
|
||||||
|
->assertFailed();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CreatePendingTenants commands all option overrides any config constraints', function () {
|
||||||
|
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
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tenancy can check if there are any pending tenants', function () {
|
||||||
|
expect(Tenant::onlyPending()->exists())->toBeFalse();
|
||||||
|
|
||||||
|
Tenant::createPending();
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->exists())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tenancy can pull a pending tenant', function () {
|
||||||
|
Tenant::createPending();
|
||||||
|
|
||||||
|
expect(Tenant::pullPendingFromPool())->toBeInstanceOf(Tenant::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pulling a tenant from the pending tenant pool removes it from the pool', function () {
|
||||||
|
Tenant::createPending();
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toEqual(1);
|
||||||
|
|
||||||
|
Tenant::pullPendingFromPool();
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a new tenant gets created while pulling a pending tenant if the pending pool is empty', function () {
|
||||||
|
expect(Tenant::withPending()->get()->count())->toBe(0); // All tenants
|
||||||
|
|
||||||
|
Tenant::pullPending();
|
||||||
|
|
||||||
|
expect(Tenant::withPending()->get()->count())->toBe(1); // All tenants
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pending tenants are included in all queries based on the include_in_queries config', function () {
|
||||||
|
Tenant::createPending();
|
||||||
|
|
||||||
|
config(['tenancy.pending.include_in_queries' => false]);
|
||||||
|
|
||||||
|
expect(Tenant::all()->count())->toBe(0);
|
||||||
|
|
||||||
|
config(['tenancy.pending.include_in_queries' => true]);
|
||||||
|
|
||||||
|
expect(Tenant::all()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pending events are dispatched', function () {
|
||||||
|
Event::fake([
|
||||||
|
CreatingPendingTenant::class,
|
||||||
|
PendingTenantCreated::class,
|
||||||
|
PullingPendingTenant::class,
|
||||||
|
PendingTenantPulled::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Tenant::createPending();
|
||||||
|
|
||||||
|
Event::assertDispatched(CreatingPendingTenant::class);
|
||||||
|
Event::assertDispatched(PendingTenantCreated::class);
|
||||||
|
|
||||||
|
Tenant::pullPending();
|
||||||
|
|
||||||
|
Event::assertDispatched(PullingPendingTenant::class);
|
||||||
|
Event::assertDispatched(PendingTenantPulled::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('commands do not run for pending tenants if tenancy.pending.include_in_queries is false and the with pending option does not get passed', function() {
|
||||||
|
config(['tenancy.pending.include_in_queries' => false]);
|
||||||
|
|
||||||
|
$tenants = collect([
|
||||||
|
Tenant::create(),
|
||||||
|
Tenant::create(),
|
||||||
|
Tenant::createPending(),
|
||||||
|
Tenant::createPending(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
pest()->artisan('tenants:migrate --with-pending');
|
||||||
|
|
||||||
|
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'");
|
||||||
|
|
||||||
|
$pendingTenants = $tenants->filter->pending();
|
||||||
|
$readyTenants = $tenants->reject->pending();
|
||||||
|
|
||||||
|
$pendingTenants->each(fn ($tenant) => $artisan->doesntExpectOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||||
|
$readyTenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||||
|
|
||||||
|
$artisan->assertExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('commands run for pending tenants too if tenancy.pending.include_in_queries is true', function() {
|
||||||
|
config(['tenancy.pending.include_in_queries' => true]);
|
||||||
|
|
||||||
|
$tenants = collect([
|
||||||
|
Tenant::create(),
|
||||||
|
Tenant::create(),
|
||||||
|
Tenant::createPending(),
|
||||||
|
Tenant::createPending(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
pest()->artisan('tenants:migrate --with-pending');
|
||||||
|
|
||||||
|
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'");
|
||||||
|
|
||||||
|
$tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||||
|
|
||||||
|
$artisan->assertExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('commands run for pending tenants too if the with pending option is passed', function() {
|
||||||
|
config(['tenancy.pending.include_in_queries' => false]);
|
||||||
|
|
||||||
|
$tenants = collect([
|
||||||
|
Tenant::create(),
|
||||||
|
Tenant::create(),
|
||||||
|
Tenant::createPending(),
|
||||||
|
Tenant::createPending(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
pest()->artisan('tenants:migrate --with-pending');
|
||||||
|
|
||||||
|
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz' --with-pending");
|
||||||
|
|
||||||
|
$tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||||
|
|
||||||
|
$artisan->assertExitCode(0);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue