diff --git a/phpstan.neon b/phpstan.neon index 17f8f56e..3e9ba51d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -31,6 +31,18 @@ parameters: message: '#PHPDoc tag \@param has invalid value \(dynamic#' paths: - src/helpers.php + - + message: '#Illuminate\\Routing\\UrlGenerator#' + paths: + - src/Bootstrappers/FilesystemTenancyBootstrapper.php + - + message: '#select\(\) expects string, Illuminate\\Database\\Query\\Expression given#' + paths: + - src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php + - + message: '#Trying to invoke Closure\|null but it might not be a callable#' + paths: + - src/Database/DatabaseConfig.php checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 39bb84c3..5b6ef4d8 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -120,7 +120,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper tenancy()->initialize($tenant); } - protected static function revertToPreviousState($event, ?Tenant &$previousTenant): void + protected static function revertToPreviousState(JobProcessed|JobFailed $event, ?Tenant &$previousTenant): void { $tenantId = $event->job->payload()['tenant_id'] ?? null; diff --git a/src/Commands/Down.php b/src/Commands/Down.php index 96ed5335..6b390957 100644 --- a/src/Commands/Down.php +++ b/src/Commands/Down.php @@ -20,7 +20,7 @@ class Down extends DownCommand protected $description = 'Put tenants into maintenance mode.'; - public function handle(): void + public function handle(): int { // The base down command is heavily used. Instead of saving the data inside a file, // the data is stored the tenant database, which means some Laravel features @@ -29,16 +29,18 @@ class Down extends DownCommand $payload = $this->getDownDatabasePayload(); // This runs for all tenants if no --tenants are specified - tenancy()->runForMultiple($this->option('tenants'), function ($tenant) use ($payload) { + tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($payload) { $this->line("Tenant: {$tenant['id']}"); $tenant->putDownForMaintenance($payload); }); $this->comment('Tenants are now in maintenance mode.'); + + return 0; } /** Get the payload to be placed in the "down" file. */ - protected function getDownDatabasePayload() + protected function getDownDatabasePayload(): array { return [ 'except' => $this->excludedPaths(), @@ -46,7 +48,7 @@ class Down extends DownCommand 'retry' => $this->getRetryTime(), 'refresh' => $this->option('refresh'), 'secret' => $this->option('secret'), - 'status' => (int) $this->option('status', 503), + 'status' => (int) ($this->option('status') ?? 503), ]; } } diff --git a/src/Commands/Link.php b/src/Commands/Link.php index 2b9ee4cf..53f3cf6f 100644 --- a/src/Commands/Link.php +++ b/src/Commands/Link.php @@ -49,8 +49,8 @@ class Link extends Command { CreateStorageSymlinksAction::handle( $tenants, - $this->option('relative') ?? false, - $this->option('force') ?? false, + (bool) ($this->option('relative') ?? false), + (bool) ($this->option('force') ?? false), ); $this->info('The links have been created.'); diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index d61083d4..d3989cc0 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -36,7 +36,7 @@ class Rollback extends RollbackCommand return 1; } - tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { + tenancy()->runForMultiple($this->getTenants(), function ($tenant) { $this->line("Tenant: {$tenant->getTenantKey()}"); event(new RollingBackDatabase($tenant)); diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 403ffd1b..9bb04716 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -6,11 +6,14 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel; +use Stancl\Tenancy\Concerns\HasATenantsOption; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; class Run extends Command { + use HasATenantsOption; + protected $description = 'Run a command for tenant(s)'; protected $signature = 'tenants:run {commandname : The artisan command.} @@ -19,7 +22,8 @@ class Run extends Command public function handle(): void { $argvInput = $this->argvInput(); - tenancy()->runForMultiple($this->option('tenants'), function ($tenant) use ($argvInput) { + + tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) { $this->line("Tenant: {$tenant->getTenantKey()}"); $this->getLaravel() @@ -30,12 +34,15 @@ class Run extends Command protected function argvInput(): ArgvInput { + /** @var string $commandname */ + $commandname = $this->argument('commandname'); + // Convert string command to array - $subCommand = explode(' ', $this->argument('commandname')); + $subcommand = explode(' ', $commandname); // Add "artisan" as first parameter because ArgvInput expects "artisan" as first parameter and later removes it - array_unshift($subCommand, 'artisan'); + array_unshift($subcommand, 'artisan'); - return new ArgvInput($subCommand); + return new ArgvInput($subcommand); } } diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index b59e0062..496c04e6 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -35,7 +35,7 @@ class Seed extends SeedCommand return 1; } - tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { + tenancy()->runForMultiple($this->getTenants(), function ($tenant) { $this->line("Tenant: {$tenant->getTenantKey()}"); event(new SeedingDatabase($tenant)); diff --git a/src/Contracts/Domain.php b/src/Contracts/Domain.php index 2c02089e..a9a19a50 100644 --- a/src/Contracts/Domain.php +++ b/src/Contracts/Domain.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\Contracts; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + /** * @property-read Tenant $tenant * @@ -15,5 +17,5 @@ namespace Stancl\Tenancy\Contracts; */ interface Domain { - public function tenant(); + public function tenant(): BelongsTo; } diff --git a/src/Contracts/TenantCannotBeCreatedException.php b/src/Contracts/TenantCannotBeCreatedException.php index 19eac15b..53d8589f 100644 --- a/src/Contracts/TenantCannotBeCreatedException.php +++ b/src/Contracts/TenantCannotBeCreatedException.php @@ -8,6 +8,7 @@ abstract class TenantCannotBeCreatedException extends \Exception { abstract public function reason(): string; + /** @var string */ protected $message; public function __construct() diff --git a/src/Controllers/TenantAssetsController.php b/src/Controllers/TenantAssetsController.php index 278493c6..615f8054 100644 --- a/src/Controllers/TenantAssetsController.php +++ b/src/Controllers/TenantAssetsController.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Controllers; use Closure; use Illuminate\Routing\Controller; +use Symfony\Component\HttpFoundation\BinaryFileResponse; use Throwable; class TenantAssetsController extends Controller // todo rename this to TenantAssetController & update references in docs @@ -17,7 +18,10 @@ class TenantAssetsController extends Controller // todo rename this to TenantAss $this->middleware(static::$tenancyMiddleware); } - public function asset(string $path = null) + /** + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function asset(string $path = null): BinaryFileResponse { abort_if($path === null, 404); diff --git a/src/Database/Contracts/TenantWithDatabase.php b/src/Database/Contracts/TenantWithDatabase.php index c9247d94..76a73340 100644 --- a/src/Database/Contracts/TenantWithDatabase.php +++ b/src/Database/Contracts/TenantWithDatabase.php @@ -9,5 +9,15 @@ use Stancl\Tenancy\Database\DatabaseConfig; interface TenantWithDatabase extends Tenant { + /** Get the tenant's database config. */ public function database(): DatabaseConfig; + + /** Get the internal prefix. */ + public static function internalPrefix(): string; + + /** Get an internal key. */ + public function getInternal(string $key): mixed; + + /** Set internal key. */ + public function setInternal(string $key, mixed $value): static; } diff --git a/src/Database/DatabaseConfig.php b/src/Database/DatabaseConfig.php index f024b442..6c68f379 100644 --- a/src/Database/DatabaseConfig.php +++ b/src/Database/DatabaseConfig.php @@ -81,7 +81,7 @@ class DatabaseConfig */ public function makeCredentials(): void { - $this->tenant->setInternal('db_name', $this->getName() ?? (static::$databaseNameGenerator)($this->tenant)); + $this->tenant->setInternal('db_name', $this->getName()); if ($this->manager() instanceof Contracts\ManagesDatabaseUsers) { $this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant)); diff --git a/src/Features/TenantConfig.php b/src/Features/TenantConfig.php index 33f1c7dd..5bc84060 100644 --- a/src/Features/TenantConfig.php +++ b/src/Features/TenantConfig.php @@ -18,7 +18,7 @@ class TenantConfig implements Feature { public array $originalConfig = []; - /** @var array */ + /** @var array */ public static array $storageToConfigMap = [ // 'paypal_api_key' => 'services.paypal.api_key', ]; @@ -30,7 +30,10 @@ class TenantConfig implements Feature public function bootstrap(Tenancy $tenancy): void { Event::listen(TenancyBootstrapped::class, function (TenancyBootstrapped $event) { - $this->setTenantConfig($event->tenancy->tenant); + /** @var Tenant $tenant */ + $tenant = $event->tenancy->tenant; + + $this->setTenantConfig($tenant); }); Event::listen(RevertedToCentralContext::class, function () { @@ -40,8 +43,8 @@ class TenantConfig implements Feature public function setTenantConfig(Tenant $tenant): void { - /** @var Tenant|Model $tenant */ foreach (static::$storageToConfigMap as $storageKey => $configKey) { + /** @var Tenant&Model $tenant */ $override = Arr::get($tenant, $storageKey); if (! is_null($override)) { diff --git a/src/Listeners/BootstrapTenancy.php b/src/Listeners/BootstrapTenancy.php index 205efc5f..50f38208 100644 --- a/src/Listeners/BootstrapTenancy.php +++ b/src/Listeners/BootstrapTenancy.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Listeners; +use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Events\BootstrappingTenancy; use Stancl\Tenancy\Events\TenancyBootstrapped; use Stancl\Tenancy\Events\TenancyInitialized; @@ -15,7 +16,10 @@ class BootstrapTenancy event(new BootstrappingTenancy($event->tenancy)); foreach ($event->tenancy->getBootstrappers() as $bootstrapper) { - $bootstrapper->bootstrap($event->tenancy->tenant); + /** @var Tenant $tenant */ + $tenant = $event->tenancy->tenant; + + $bootstrapper->bootstrap($tenant); } event(new TenancyBootstrapped($event->tenancy)); diff --git a/src/Tenancy.php b/src/Tenancy.php index c93df4fb..5fe6bd52 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -99,19 +99,30 @@ class Tenancy { $class = config('tenancy.tenant_model'); - return new $class; + /** @var Tenant&Model $model */ + $model = new $class; + + return $model; } + /** + * Try to find a tenant using an ID. + * + * @return (Tenant&Model)|null + */ public static function find(int|string $id): Tenant|null { - return static::model()->where(static::model()->getTenantKeyName(), $id)->first(); + /** @var (Tenant&Model)|null */ + $tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first(); + + return $tenant; } /** * Run a callback in the central context. * Atomic, safely reverts to previous context. */ - public function central(Closure $callback) + public function central(Closure $callback): mixed { $previousTenant = $this->tenant; @@ -132,7 +143,7 @@ class Tenancy * Run a callback for multiple tenants. * More performant than running $tenant->run() one by one. * - * @param array|array|\Traversable|null $tenants + * @param array|array|\Traversable|string|int|null $tenants */ public function runForMultiple($tenants, Closure $callback): void { @@ -155,6 +166,7 @@ class Tenancy $tenant = $this->find($tenant); } + /** @var Tenant $tenant */ $this->initialize($tenant); $callback($tenant); } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index f7785bf2..219d87b4 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Illuminate\Database\DatabaseManager; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; @@ -201,8 +202,9 @@ test('run command with array of tenants works', function () { Artisan::call('tenants:migrate-fresh'); pest()->artisan("tenants:run --tenants=$tenantId1 --tenants=$tenantId2 'foo foo --b=bar --c=xyz'") - ->expectsOutput('Tenant: ' . $tenantId1) - ->expectsOutput('Tenant: ' . $tenantId2); + ->expectsOutputToContain('Tenant: ' . $tenantId1) + ->expectsOutputToContain('Tenant: ' . $tenantId2) + ->assertExitCode(0); }); test('link command works', function() {