1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 17:04:04 +00:00

[4.x] Add pending tenants (modified #782) (#869)

* 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 commit 547440c887.

* Add a comment line about the possible tenant symlinks

* Correct storagePath and publicPath variables

* Revert "Correct storagePath and publicPath variables"

This reverts commit e3aa8e2086.

* 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:
Samuel Štancl 2022-10-31 12:14:44 +01:00 committed by GitHub
parent bf504f4c79
commit 198f34f5e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 693 additions and 29 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
.env .env
.DS_Store
composer.lock composer.lock
vendor/ vendor/
.vscode/ .vscode/

View file

@ -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 => [],

View file

@ -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.
*/ */

View file

@ -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",

View 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.');
}
}

View 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();
}
}

View file

@ -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}

View file

@ -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}

View file

@ -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)';

View file

@ -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)';

View file

@ -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).';

View file

@ -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)';

View file

@ -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).';

View file

@ -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';

View file

@ -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();
} }

View 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;
}
}

View 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;
});
}
}

View file

@ -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;

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class CreatingPendingTenant extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class PendingTenantCreated extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class PendingTenantPulled extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class PullingPendingTenant extends Contracts\TenantEvent
{
}

View 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);
}
}

View 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);
}
}

View file

@ -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;

View file

@ -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 () {

View file

@ -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,

View file

@ -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.

View file

@ -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;
} }

View 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);
});