diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d76a686a..03aa4ee8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,8 @@ Run `composer docker-up` to start the containers. Then run `composer test` to ru If you need to pass additional flags to phpunit, use `./test --foo` instead of `composer test --foo`. Composer scripts unfortunately don't pass CLI arguments. +If you want to run a specific test (or test file), you can also use `./t 'name of the test'`. This is equivalent to `./test --no-coverage --filter 'name of the test'`. + When you're done testing, run `composer docker-down` to shut down the containers. ### Docker on M1 diff --git a/phpstan.neon b/phpstan.neon index 0567d5ff..a6bce96d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,14 +16,17 @@ parameters: ignoreErrors: - '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#' - '#Cannot access offset (.*?) on Illuminate\\Contracts\\Config\\Repository#' + - + message: '#Call to an undefined (method|static method) Illuminate\\Database\\Eloquent\\(Model|Builder)#' + paths: + - src/Commands/CreatePendingTenants.php + - src/Commands/ClearPendingTenants.php + - src/Database/Concerns/PendingScope.php + - src/Database/ParentModelScope.php - message: '#invalid type Laravel\\Telescope\\IncomingEntry#' paths: - src/Features/TelescopeTags.php - - - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getRelationshipToPrimaryModel\(\)#' - paths: - - src/Database/ParentModelScope.php - message: '#Parameter \#1 \$key of method Illuminate\\Contracts\\Cache\\Repository::put\(\) expects string#' paths: @@ -44,6 +47,7 @@ parameters: message: '#Trying to invoke Closure\|null but it might not be a callable#' paths: - src/Database/DatabaseConfig.php + - '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#' checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false diff --git a/src/Commands/ClearPendingTenants.php b/src/Commands/ClearPendingTenants.php index 18d9fa42..19d31195 100644 --- a/src/Commands/ClearPendingTenants.php +++ b/src/Commands/ClearPendingTenants.php @@ -9,27 +9,14 @@ 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() + public function handle(): int { $this->info('Removing pending tenants.'); @@ -39,7 +26,10 @@ class ClearPendingTenants extends Command // Skip the time constraints if the 'all' option is given if (! $this->option('all')) { + /** @var ?int $olderThanDays */ $olderThanDays = $this->option('older-than-days'); + + /** @var ?int $olderThanHours */ $olderThanHours = $this->option('older-than-hours'); if ($olderThanDays && $olderThanHours) { @@ -70,5 +60,7 @@ class ClearPendingTenants extends Command ->count(); $this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.'); + + return 0; } } diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php index 88202093..7b2c7934 100644 --- a/src/Commands/CreatePendingTenants.php +++ b/src/Commands/CreatePendingTenants.php @@ -8,24 +8,11 @@ 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() + public function handle(): int { $this->info('Creating pending tenants.'); @@ -46,13 +33,11 @@ class CreatePendingTenants extends Command $this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.'); $this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); - return 1; + return 0; } - /** - * Calculate the number of currently available pending tenants. - */ - private function getPendingTenantCount(): int + /** Calculate the number of currently available pending tenants. */ + protected function getPendingTenantCount(): int { return tenancy() ->query() diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 77c96588..c7041a72 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -117,6 +117,7 @@ class Install extends Command $this->newLine(); } } else { + /** @var string $warning */ $this->components->warn($warning); } } diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 45a93115..7df75fb0 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; -use Illuminate\Console\Command; +use Illuminate\Database\Console\Migrations\BaseCommand; use Stancl\Tenancy\Concerns\DealsWithMigrations; use Stancl\Tenancy\Concerns\HasTenantOptions; use Symfony\Component\Console\Input\InputOption; -class MigrateFresh extends Command +class MigrateFresh extends BaseCommand { use HasTenantOptions, DealsWithMigrations; diff --git a/src/Concerns/DealsWithMigrations.php b/src/Concerns/DealsWithMigrations.php index 3129c68d..3a757271 100644 --- a/src/Concerns/DealsWithMigrations.php +++ b/src/Concerns/DealsWithMigrations.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace Stancl\Tenancy\Concerns; +/** + * @mixin \Illuminate\Database\Console\Migrations\BaseCommand + */ trait DealsWithMigrations { protected function getMigrationPaths(): array diff --git a/src/Contracts/Syncable.php b/src/Contracts/Syncable.php index a481f318..f8e7fd84 100644 --- a/src/Contracts/Syncable.php +++ b/src/Contracts/Syncable.php @@ -18,4 +18,6 @@ interface Syncable /** Get the attributes used for creating the *other* model (i.e. tenant if this is the central one, and central if this is the tenant one). */ public function getSyncedCreationAttributes(): array|null; // todo come up with a better name + + public function shouldSync(): bool; } diff --git a/src/Database/Concerns/HasPending.php b/src/Database/Concerns/HasPending.php index 3fa9399d..4d72486f 100644 --- a/src/Database/Concerns/HasPending.php +++ b/src/Database/Concerns/HasPending.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Events\CreatingPendingTenant; use Stancl\Tenancy\Events\PendingTenantCreated; @@ -14,7 +15,7 @@ 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 + * @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() @@ -22,38 +23,30 @@ use Stancl\Tenancy\Events\PullingPendingTenant; */ trait HasPending { - /** - * Boot the has pending trait for a model. - * - * @return void - */ - public static function bootHasPending() + /** Boot the trait. */ + public static function bootHasPending(): void { static::addGlobalScope(new PendingScope()); } - /** - * Initialize the has pending trait for an instance. - * - * @return void - */ - public function initializeHasPending() + /** Initialize the trait. */ + public function initializeHasPending(): void { $this->casts['pending_since'] = 'timestamp'; } - /** - * Determine if the model instance is in a pending state. - * - * @return bool - */ - public function pending() + /** Determine if the model instance is in a pending state. */ + public function pending(): bool { return ! is_null($this->pending_since); } - /** Create a pending tenant. */ - public static function createPending($attributes = []): Tenant + /** + * Create a pending tenant. + * + * @param array $attributes + */ + public static function createPending(array $attributes = []): Model&Tenant { $tenant = static::create($attributes); @@ -71,9 +64,12 @@ trait HasPending } /** Pull a pending tenant. */ - public static function pullPending(): Tenant + public static function pullPending(): Model&Tenant { - return static::pullPendingFromPool(true); + /** @var Model&Tenant $pendingTenant */ + $pendingTenant = static::pullPendingFromPool(true); + + return $pendingTenant; } /** Try to pull a tenant from the pool of pending tenants. */ @@ -88,6 +84,7 @@ trait HasPending } // A pending tenant is surely available at this point + /** @var Model&Tenant $tenant */ $tenant = static::onlyPending()->first(); event(new PullingPendingTenant($tenant)); diff --git a/src/Database/Concerns/ResourceSyncing.php b/src/Database/Concerns/ResourceSyncing.php index fd63738d..ea9f83b4 100644 --- a/src/Database/Concerns/ResourceSyncing.php +++ b/src/Database/Concerns/ResourceSyncing.php @@ -13,8 +13,9 @@ trait ResourceSyncing public static function bootResourceSyncing(): void { static::saved(function (Syncable $model) { - /** @var ResourceSyncing $model */ - $model->triggerSyncEvent(); + if ($model->shouldSync()) { + $model->triggerSyncEvent(); + } }); static::creating(function (self $model) { @@ -37,4 +38,9 @@ trait ResourceSyncing { return null; } + + public function shouldSync(): bool + { + return true; + } } diff --git a/src/Database/Models/TenantPivot.php b/src/Database/Models/TenantPivot.php index 2c7583c1..3cc614a9 100644 --- a/src/Database/Models/TenantPivot.php +++ b/src/Database/Models/TenantPivot.php @@ -14,7 +14,7 @@ class TenantPivot extends Pivot static::saved(function (self $pivot) { $parent = $pivot->pivotParent; - if ($parent instanceof Syncable) { + if ($parent instanceof Syncable && $parent->shouldSync()) { $parent->triggerSyncEvent(); } }); diff --git a/src/Jobs/ClearPendingTenants.php b/src/Jobs/ClearPendingTenants.php index 7cd78495..773e3e93 100644 --- a/src/Jobs/ClearPendingTenants.php +++ b/src/Jobs/ClearPendingTenants.php @@ -16,12 +16,7 @@ class ClearPendingTenants implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** - * Execute the job. - * - * @return void - */ - public function handle() + public function handle(): void { Artisan::call(ClearPendingTenantsCommand::class); } diff --git a/src/Jobs/CreatePendingTenants.php b/src/Jobs/CreatePendingTenants.php index 8f3da218..81199761 100644 --- a/src/Jobs/CreatePendingTenants.php +++ b/src/Jobs/CreatePendingTenants.php @@ -16,12 +16,7 @@ class CreatePendingTenants implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** - * Execute the job. - * - * @return void - */ - public function handle() + public function handle(): void { Artisan::call(CreatePendingTenantsCommand::class); } diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index 3e484f87..e73605e3 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -45,8 +45,6 @@ class InitializeTenancyByPath extends IdentificationMiddleware } else { throw new RouteIsMissingTenantParameterException; } - - return $next($request); } protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void diff --git a/src/Middleware/InitializeTenancyByRequestData.php b/src/Middleware/InitializeTenancyByRequestData.php index ca29f3d7..925907f0 100644 --- a/src/Middleware/InitializeTenancyByRequestData.php +++ b/src/Middleware/InitializeTenancyByRequestData.php @@ -34,18 +34,17 @@ class InitializeTenancyByRequestData extends IdentificationMiddleware protected function getPayload(Request $request): ?string { + $payload = null; + if (static::$header && $request->hasHeader(static::$header)) { - return $request->header(static::$header); + $payload = $request->header(static::$header); + } elseif (static::$queryParameter && $request->has(static::$queryParameter)) { + $payload = $request->get(static::$queryParameter); + } elseif (static::$cookie && $request->hasCookie(static::$cookie)) { + $payload = $request->cookie(static::$cookie); } - if (static::$queryParameter && $request->has(static::$queryParameter)) { - return $request->get(static::$queryParameter); - } - - if (static::$cookie && $request->hasCookie(static::$cookie)) { - return $request->cookie(static::$cookie); - } - - return null; + /** @var ?string $payload */ + return $payload; } } diff --git a/src/Tenancy.php b/src/Tenancy.php index e95e0059..5b30e3e0 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -42,8 +42,7 @@ class Tenancy } } - // todo1 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property - if ($this->initialized && $this->tenant->getTenantKey() === $tenant->getTenantKey()) { + if ($this->initialized && $this->tenant?->getTenantKey() === $tenant->getTenantKey()) { return; } @@ -52,6 +51,7 @@ class Tenancy $this->end(); } + /** @var Tenant&Model $tenant */ $this->tenant = $tenant; event(new Events\InitializingTenancy($this)); diff --git a/t b/t new file mode 100755 index 00000000..3c74f2e8 --- /dev/null +++ b/t @@ -0,0 +1,3 @@ +#!/bin/bash + +docker-compose exec -T test vendor/bin/pest --no-coverage --filter "$@" diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 430c52ef..e1586bc1 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -763,6 +763,43 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase() expect(ResourceUser::first()->role)->toBe('commenter'); } +test('resources are synced only when sync is enabled', function (bool $enabled) { + app()->instance('_tenancy_test_shouldSync', $enabled); + + [$tenant1, $tenant2] = createTenantsAndRunMigrations(); + migrateUsersTableForTenants(); + + tenancy()->initialize($tenant1); + + TenantUserWithConditionalSync::create([ + 'global_id' => 'absd', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commenter', + ]); + + tenancy()->end(); + + expect(CentralUserWithConditionalSync::all())->toHaveCount($enabled ? 1 : 0); + expect(CentralUserWithConditionalSync::whereGlobalId('absd')->exists())->toBe($enabled); + + $centralUser = CentralUserWithConditionalSync::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commenter', + ]); + + $centralUser->tenants()->attach('t2'); + + $tenant2->run(function () use ($enabled) { + expect(TenantUserWithConditionalSync::all())->toHaveCount($enabled ? 1 : 0); + expect(TenantUserWithConditionalSync::whereGlobalId('acme')->exists())->toBe($enabled); + }); +})->with([[true], [false]]); + /** * Create two tenants and run migrations for those tenants. */ @@ -979,3 +1016,19 @@ class ResourceUserProvidingMixture extends ResourceUser ]; } } + +class CentralUserWithConditionalSync extends CentralUser +{ + public function shouldSync(): bool + { + return app('_tenancy_test_shouldSync'); + } +} + +class TenantUserWithConditionalSync extends ResourceUser +{ + public function shouldSync(): bool + { + return app('_tenancy_test_shouldSync'); + } +}