diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 226229c0..92c381c4 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -150,16 +150,8 @@ class TenancyServiceProvider extends ServiceProvider protected function makeTenancyMiddlewareHighestPriority() { - $tenancyMiddleware = [ - // Even higher priority than the initialization middleware - Middleware\PreventAccessFromCentralDomains::class, - - Middleware\InitializeTenancyByDomain::class, - Middleware\InitializeTenancyBySubdomain::class, - Middleware\InitializeTenancyByDomainOrSubdomain::class, - Middleware\InitializeTenancyByPath::class, - Middleware\InitializeTenancyByRequestData::class, - ]; + // PreventAccessFromCentralDomains has even higher priority than the identification middleware + $tenancyMiddleware = array_merge([Middleware\PreventAccessFromCentralDomains::class], config('tenancy.identification.middleware')); foreach (array_reverse($tenancyMiddleware) as $middleware) { $this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware); diff --git a/assets/config.php b/assets/config.php index 282e7d32..a67a85bf 100644 --- a/assets/config.php +++ b/assets/config.php @@ -4,6 +4,8 @@ declare(strict_types=1); use Stancl\Tenancy\Database\Models\Domain; use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Middleware; +use Stancl\Tenancy\Resolvers; return [ 'tenant_model' => Tenant::class, @@ -21,6 +23,56 @@ return [ 'localhost', ], + 'identification' => [ + /** + * The default middleware used for tenant identification. + * + * If you use multiple forms of identification, you can set this to the "main" approach you use. + */ + 'default_middleware' => Middleware\InitializeTenancyByDomain::class,// todo@identification add this to a 'tenancy' mw group + + /** + * All of the identification middleware used by the package. + * + * If you write your own, make sure to add them to this array. + */ + 'middleware' => [ + Middleware\InitializeTenancyByDomain::class, + Middleware\InitializeTenancyBySubdomain::class, + Middleware\InitializeTenancyByDomainOrSubdomain::class, + Middleware\InitializeTenancyByPath::class, + Middleware\InitializeTenancyByRequestData::class, + ], + + /** + * Tenant resolvers used by the package. + * + * Resolvers which implement the CachedTenantResolver contract have options for configuring the caching details. + * If you add your own resolvers, do not add the 'cache' key unless your resolver is based on CachedTenantResolver. + */ + 'resolvers' => [ + Resolvers\DomainTenantResolver::class => [ + 'cache' => false, + 'cache_ttl' => 3600, // seconds + 'cache_store' => null, // default + ], + Resolvers\PathTenantResolver::class => [ + 'tenant_parameter_name' => 'tenant', + + 'cache' => false, + 'cache_ttl' => 3600, // seconds + 'cache_store' => null, // default + ], + Resolvers\RequestDataTenantResolver::class => [ + 'cache' => false, + 'cache_ttl' => 3600, // seconds + 'cache_store' => null, // default + ], + ], + + // todo@docs update integration guides to use Stancl\Tenancy::defaultMiddleware() + ], + /** * Tenancy bootstrappers are executed when tenancy is initialized. * Their responsibility is making Laravel features tenant-aware. @@ -234,4 +286,12 @@ return [ '--class' => 'DatabaseSeeder', // root seeder class // '--force' => true, ], + + /** + * Single-database tenancy config. + */ + 'single_db' => [ + /** The name of the column used by models with the BelongsToTenant trait. */ + 'tenant_id_column' => 'tenant_id', + ], ]; diff --git a/assets/routes.php b/assets/routes.php index 9223c099..a27f782d 100644 --- a/assets/routes.php +++ b/assets/routes.php @@ -3,7 +3,8 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; +use Stancl\Tenancy\Controllers\TenantAssetController; -Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset') +Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset']) ->where('path', '(.*)') ->name('stancl.tenancy.asset'); diff --git a/phpstan.neon b/phpstan.neon index f325f3ec..3e9ba51d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,14 +13,16 @@ parameters: - Illuminate\Database\Eloquent\Model ignoreErrors: - - - message: '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#' - paths: - - src/TenancyServiceProvider.php + - '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#' + - '#Cannot access offset (.*?) on Illuminate\\Contracts\\Config\\Repository#' - 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: @@ -29,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/phpunit.xml b/phpunit.xml index 28fc8a08..9d2b9339 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/src/Bootstrappers/BatchTenancyBootstrapper.php b/src/Bootstrappers/BatchTenancyBootstrapper.php index ccd1c00a..589bdac0 100644 --- a/src/Bootstrappers/BatchTenancyBootstrapper.php +++ b/src/Bootstrappers/BatchTenancyBootstrapper.php @@ -23,14 +23,14 @@ class BatchTenancyBootstrapper implements TenancyBootstrapper ) { } - public function bootstrap(Tenant $tenant) + public function bootstrap(Tenant $tenant): void { // Update batch repository connection to use the tenant connection $this->previousConnection = $this->batchRepository->getConnection(); $this->batchRepository->setConnection($this->databaseManager->connection('tenant')); } - public function revert() + public function revert(): void { if ($this->previousConnection) { // Replace batch repository connection with the previously replaced one diff --git a/src/Bootstrappers/CacheTenancyBootstrapper.php b/src/Bootstrappers/CacheTenancyBootstrapper.php index bef156d2..29547fae 100644 --- a/src/Bootstrappers/CacheTenancyBootstrapper.php +++ b/src/Bootstrappers/CacheTenancyBootstrapper.php @@ -13,18 +13,14 @@ use Stancl\Tenancy\Contracts\Tenant; class CacheTenancyBootstrapper implements TenancyBootstrapper { - /** @var CacheManager */ - protected $originalCache; + protected ?CacheManager $originalCache = null; - /** @var Application */ - protected $app; - - public function __construct(Application $app) - { - $this->app = $app; + public function __construct( + protected Application $app + ) { } - public function bootstrap(Tenant $tenant) + public function bootstrap(Tenant $tenant): void { $this->resetFacadeCache(); @@ -34,7 +30,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper }); } - public function revert() + public function revert(): void { $this->resetFacadeCache(); @@ -50,7 +46,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper * facade has been made prior to bootstrapping tenancy. The * facade has its own cache, separate from the container. */ - public function resetFacadeCache() + public function resetFacadeCache(): void { Cache::clearResolvedInstances(); } diff --git a/src/Bootstrappers/DatabaseTenancyBootstrapper.php b/src/Bootstrappers/DatabaseTenancyBootstrapper.php index dd94bfd4..c6dba079 100644 --- a/src/Bootstrappers/DatabaseTenancyBootstrapper.php +++ b/src/Bootstrappers/DatabaseTenancyBootstrapper.php @@ -8,7 +8,7 @@ use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\DatabaseManager; -use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException; +use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException; class DatabaseTenancyBootstrapper implements TenancyBootstrapper { @@ -20,7 +20,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper $this->database = $database; } - public function bootstrap(Tenant $tenant) + public function bootstrap(Tenant $tenant): void { /** @var TenantWithDatabase $tenant */ @@ -35,7 +35,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper $this->database->connectToTenant($tenant); } - public function revert() + public function revert(): void { $this->database->reconnectToCentral(); } diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index d90d36d0..e9e0d93d 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Routing\UrlGenerator; use Illuminate\Support\Facades\Storage; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; @@ -27,13 +28,14 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper ]; $this->app['url']->macro('setAssetRoot', function ($root) { + /** @var UrlGenerator $this */ $this->assetRoot = $root; return $this; }); } - public function bootstrap(Tenant $tenant) + public function bootstrap(Tenant $tenant): void { $suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->getTenantKey(); @@ -45,7 +47,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper // asset() if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy'] ?? true) { if ($this->originalPaths['asset_url']) { - $this->app['config']['app.asset_url'] = ($this->originalPaths['asset_url'] ?? $this->app['config']['app.url']) . "/$suffix"; + $this->app['config']['app.asset_url'] = $this->originalPaths['asset_url'] . "/$suffix"; $this->app['url']->setAssetRoot($this->app['config']['app.asset_url']); } else { $this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => ''])); @@ -82,7 +84,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper if ($url = str_replace( '%tenant_id%', - $tenant->getTenantKey(), + (string) $tenant->getTenantKey(), $this->app['config']["tenancy.filesystem.url_override.{$disk}"] ?? '' )) { $this->app['config']["filesystems.disks.{$disk}.url"] = url($url); @@ -91,7 +93,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper } } - public function revert() + public function revert(): void { // storage_path() $this->app->useStoragePath($this->originalPaths['storage']); diff --git a/src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php b/src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php index 49869bb5..da5a921a 100644 --- a/src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php +++ b/src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php @@ -10,27 +10,23 @@ use Stancl\Tenancy\Contracts\Tenant; class ScoutTenancyBootstrapper implements TenancyBootstrapper { - /** @var Repository */ - protected $config; + protected ?string $originalScoutPrefix = null; - /** @var string */ - protected $originalScoutPrefix; - - public function __construct(Repository $config) - { - $this->config = $config; + public function __construct( + protected Repository $config, + ) { } - public function bootstrap(Tenant $tenant) + public function bootstrap(Tenant $tenant): void { - if (! isset($this->originalScoutPrefix)) { + if ($this->originalScoutPrefix !== null) { $this->originalScoutPrefix = $this->config->get('scout.prefix'); } $this->config->set('scout.prefix', $this->getTenantPrefix($tenant)); } - public function revert() + public function revert(): void { $this->config->set('scout.prefix', $this->originalScoutPrefix); } diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 2f859ecd..5b6ef4d8 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -39,7 +39,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper * However, we're registering a hook to initialize tenancy. Therefore, * we need to register the hook at service provider execution time. */ - public static function __constructStatic(Application $app) + public static function __constructStatic(Application $app): void { static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests()); } @@ -52,7 +52,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper $this->setUpPayloadGenerator(); } - protected static function setUpJobListener($dispatcher, $runningTests) + protected static function setUpJobListener(Dispatcher $dispatcher, bool $runningTests): void { $previousTenant = null; @@ -62,14 +62,11 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null); }); - if (version_compare(app()->version(), '8.64', '>=')) { - // JobRetryRequested only exists since Laravel 8.64 - $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { - $previousTenant = tenant(); + $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { + $previousTenant = tenant(); - static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); - }); - } + static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); + }); // If we're running tests, we make sure to clean up after any artisan('queue:work') calls $revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) { @@ -82,7 +79,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper $dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails } - protected static function initializeTenancyForQueue($tenantId) + protected static function initializeTenancyForQueue(string|int $tenantId): void { if (! $tenantId) { // The job is not tenant-aware @@ -100,7 +97,9 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper tenancy()->end(); } - tenancy()->initialize(tenancy()->find($tenantId)); + /** @var Tenant $tenant */ + $tenant = tenancy()->find($tenantId); + tenancy()->initialize($tenant); return; } @@ -115,10 +114,13 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper // Tenancy was either not initialized, or initialized for a different tenant. // Therefore, we initialize it for the correct tenant. - tenancy()->initialize(tenancy()->find($tenantId)); + + /** @var Tenant $tenant */ + $tenant = tenancy()->find($tenantId); + tenancy()->initialize($tenant); } - protected static function revertToPreviousState($event, &$previousTenant) + protected static function revertToPreviousState(JobProcessed|JobFailed $event, ?Tenant &$previousTenant): void { $tenantId = $event->job->payload()['tenant_id'] ?? null; @@ -138,7 +140,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper } } - protected function setUpPayloadGenerator() + protected function setUpPayloadGenerator(): void { $bootstrapper = &$this; @@ -149,17 +151,17 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper } } - public function bootstrap(Tenant $tenant) + public function bootstrap(Tenant $tenant): void { // } - public function revert() + public function revert(): void { // } - public function getPayload(string $connection) + public function getPayload(string $connection): array { if (! tenancy()->initialized) { return []; diff --git a/src/Bootstrappers/RedisTenancyBootstrapper.php b/src/Bootstrappers/RedisTenancyBootstrapper.php index 7536984e..975a37d5 100644 --- a/src/Bootstrappers/RedisTenancyBootstrapper.php +++ b/src/Bootstrappers/RedisTenancyBootstrapper.php @@ -22,18 +22,21 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper $this->config = $config; } - public function bootstrap(Tenant $tenant) + public function bootstrap(Tenant $tenant): void { foreach ($this->prefixedConnections() as $connection) { $prefix = $this->config['tenancy.redis.prefix_base'] . $tenant->getTenantKey(); $client = Redis::connection($connection)->client(); - $this->originalPrefixes[$connection] = $client->getOption($client::OPT_PREFIX); + /** @var string $originalPrefix */ + $originalPrefix = $client->getOption($client::OPT_PREFIX); + + $this->originalPrefixes[$connection] = $originalPrefix; $client->setOption($client::OPT_PREFIX, $prefix); } } - public function revert() + public function revert(): void { foreach ($this->prefixedConnections() as $connection) { $client = Redis::connection($connection)->client(); @@ -44,7 +47,8 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper $this->originalPrefixes = []; } - protected function prefixedConnections() + /** @return string[] */ + protected function prefixedConnections(): array { return $this->config['tenancy.redis.prefixed_connections']; } diff --git a/src/Commands/Down.php b/src/Commands/Down.php index 5e8975c6..5531bb7c 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 0a66e67e..7a8d05ff 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/Run.php b/src/Commands/Run.php index 5052e168..0ced4d00 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -13,11 +13,7 @@ use Symfony\Component\Console\Output\ConsoleOutput; class Run extends Command { use HasTenantOptions; - /** - * The console command description. - * - * @var string - */ + protected $description = 'Run a command for tenant(s)'; protected $signature = 'tenants:run {commandname : The artisan command.} @@ -25,10 +21,9 @@ class Run extends Command public function handle(): void { - $argvInput = $this->ArgvInput(); - $tenants = $this->getTenants(); + $argvInput = $this->argvInput(); - tenancy()->runForMultiple($tenants, function ($tenant) use ($argvInput) { + tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) { $this->line("Tenant: {$tenant->getTenantKey()}"); $this->getLaravel() @@ -39,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/TenantList.php b/src/Commands/TenantList.php index ce8dfcec..9fd3f8bd 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Contracts\Tenant; class TenantList extends Command @@ -16,15 +17,16 @@ class TenantList extends Command public function handle(): void { $this->info('Listing all tenants.'); - tenancy() - ->query() - ->cursor() - ->each(function (Tenant $tenant) { - if ($tenant->domains) { - $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); - } else { - $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"); - } - }); + + $tenants = tenancy()->query()->cursor(); + + foreach ($tenants as $tenant) { + /** @var Model&Tenant $tenant */ + if ($tenant->domains) { + $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); + } else { + $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"); + } + } } } diff --git a/src/Concerns/DealsWithTenantSymlinks.php b/src/Concerns/DealsWithTenantSymlinks.php index d6d6f5f2..5f3baf5b 100644 --- a/src/Concerns/DealsWithTenantSymlinks.php +++ b/src/Concerns/DealsWithTenantSymlinks.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Concerns; use Illuminate\Support\Collection; -use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Contracts\Tenant; trait DealsWithTenantSymlinks { @@ -23,12 +23,14 @@ trait DealsWithTenantSymlinks $diskUrls = config('tenancy.filesystem.url_override'); $disks = config('tenancy.filesystem.root_override'); $suffixBase = config('tenancy.filesystem.suffix_base'); - $symlinks = collect(); $tenantKey = $tenant->getTenantKey(); + /** @var Collection> $symlinks */ + $symlinks = collect([]); + foreach ($diskUrls as $disk => $publicPath) { $storagePath = str_replace('%storage_path%', $suffixBase . $tenantKey, $disks[$disk]); - $publicPath = str_replace('%tenant_id%', $tenantKey, $publicPath); + $publicPath = str_replace('%tenant_id%', (string) $tenantKey, $publicPath); tenancy()->central(function () use ($symlinks, $publicPath, $storagePath) { $symlinks->push([public_path($publicPath) => storage_path($storagePath)]); 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/TenancyBootstrapper.php b/src/Contracts/TenancyBootstrapper.php index 8b43755f..6da5c537 100644 --- a/src/Contracts/TenancyBootstrapper.php +++ b/src/Contracts/TenancyBootstrapper.php @@ -9,7 +9,7 @@ namespace Stancl\Tenancy\Contracts; */ interface TenancyBootstrapper { - public function bootstrap(Tenant $tenant); + public function bootstrap(Tenant $tenant): void; - public function revert(); + public function revert(): void; } 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/TenantAssetController.php b/src/Controllers/TenantAssetController.php new file mode 100644 index 00000000..7a95dffe --- /dev/null +++ b/src/Controllers/TenantAssetController.php @@ -0,0 +1,32 @@ +middleware(Tenancy::defaultMiddleware()); + } + + /** + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function asset(string $path = null): BinaryFileResponse + { + abort_if($path === null, 404); + + try { + return response()->file(storage_path("app/public/$path")); + } catch (Throwable) { + abort(404); + } + } +} diff --git a/src/Controllers/TenantAssetsController.php b/src/Controllers/TenantAssetsController.php deleted file mode 100644 index 7e032c2c..00000000 --- a/src/Controllers/TenantAssetsController.php +++ /dev/null @@ -1,30 +0,0 @@ -middleware(static::$tenancyMiddleware); - } - - public function asset(string $path = null) - { - abort_if($path === null, 404); - - try { - return response()->file(storage_path("app/public/$path")); - } catch (Throwable) { - abort(404); - } - } -} diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index ade966a8..07048a1f 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -12,11 +12,14 @@ use Stancl\Tenancy\Database\TenantScope; */ trait BelongsToTenant { - public static $tenantIdColumn = 'tenant_id'; - public function tenant() { - return $this->belongsTo(config('tenancy.tenant_model'), BelongsToTenant::$tenantIdColumn); + return $this->belongsTo(config('tenancy.tenant_model'), static::tenantIdColumn()); + } + + public static function tenantIdColumn(): string + { + return config('tenancy.single_db.tenant_id_column'); } public static function bootBelongsToTenant(): void @@ -24,9 +27,9 @@ trait BelongsToTenant static::addGlobalScope(new TenantScope); static::creating(function ($model) { - if (! $model->getAttribute(BelongsToTenant::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { + if (! $model->getAttribute(static::tenantIdColumn()) && ! $model->relationLoaded('tenant')) { if (tenancy()->initialized) { - $model->setAttribute(BelongsToTenant::$tenantIdColumn, tenant()->getTenantKey()); + $model->setAttribute(static::tenantIdColumn(), tenant()->getTenantKey()); $model->setRelation('tenant', tenant()); } } diff --git a/src/Database/Concerns/HasScopedValidationRules.php b/src/Database/Concerns/HasScopedValidationRules.php index ae5c7fc7..7913a215 100644 --- a/src/Database/Concerns/HasScopedValidationRules.php +++ b/src/Database/Concerns/HasScopedValidationRules.php @@ -11,11 +11,11 @@ trait HasScopedValidationRules { public function unique($table, $column = 'NULL') { - return (new Unique($table, $column))->where(BelongsToTenant::$tenantIdColumn, $this->getTenantKey()); + return (new Unique($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey()); } public function exists($table, $column = 'NULL') { - return (new Exists($table, $column))->where(BelongsToTenant::$tenantIdColumn, $this->getTenantKey()); + return (new Exists($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey()); } } diff --git a/src/Database/Concerns/InvalidatesResolverCache.php b/src/Database/Concerns/InvalidatesResolverCache.php index 3b864789..21894f41 100644 --- a/src/Database/Concerns/InvalidatesResolverCache.php +++ b/src/Database/Concerns/InvalidatesResolverCache.php @@ -5,21 +5,15 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Tenant; -use Stancl\Tenancy\Resolvers; use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver; +use Stancl\Tenancy\Tenancy; trait InvalidatesResolverCache { - public static $resolvers = [ - Resolvers\DomainTenantResolver::class, - Resolvers\PathTenantResolver::class, - Resolvers\RequestDataTenantResolver::class, - ]; - public static function bootInvalidatesResolverCache(): void { static::saved(function (Tenant $tenant) { - foreach (static::$resolvers as $resolver) { + foreach (Tenancy::cachedResolvers() as $resolver) { /** @var CachedTenantResolver $resolver */ $resolver = app($resolver); diff --git a/src/Database/Concerns/InvalidatesTenantsResolverCache.php b/src/Database/Concerns/InvalidatesTenantsResolverCache.php index 8d7c2845..d954567f 100644 --- a/src/Database/Concerns/InvalidatesTenantsResolverCache.php +++ b/src/Database/Concerns/InvalidatesTenantsResolverCache.php @@ -5,24 +5,18 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; use Illuminate\Database\Eloquent\Model; -use Stancl\Tenancy\Resolvers; use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver; +use Stancl\Tenancy\Tenancy; /** * Meant to be used on models that belong to tenants. */ trait InvalidatesTenantsResolverCache { - public static $resolvers = [ - Resolvers\DomainTenantResolver::class, - Resolvers\PathTenantResolver::class, - Resolvers\RequestDataTenantResolver::class, - ]; - public static function bootInvalidatesTenantsResolverCache(): void { static::saved(function (Model $model) { - foreach (static::$resolvers as $resolver) { + foreach (Tenancy::cachedResolvers() as $resolver) { /** @var CachedTenantResolver $resolver */ $resolver = app($resolver); 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 a4c79582..6c68f379 100644 --- a/src/Database/DatabaseConfig.php +++ b/src/Database/DatabaseConfig.php @@ -26,20 +26,20 @@ class DatabaseConfig public static function __constructStatic(): void { - static::$usernameGenerator = static::$usernameGenerator ?? function (Tenant $tenant) { + static::$usernameGenerator = static::$usernameGenerator ?? function (Model&Tenant $tenant) { return Str::random(16); }; - static::$passwordGenerator = static::$passwordGenerator ?? function (Tenant $tenant) { + static::$passwordGenerator = static::$passwordGenerator ?? function (Model&Tenant $tenant) { return Hash::make(Str::random(32)); }; - static::$databaseNameGenerator = static::$databaseNameGenerator ?? function (Tenant $tenant) { + static::$databaseNameGenerator = static::$databaseNameGenerator ?? function (Model&Tenant $tenant) { return config('tenancy.database.prefix') . $tenant->getTenantKey() . config('tenancy.database.suffix'); }; } - public function __construct(Tenant $tenant) + public function __construct(Model&Tenant $tenant) { static::__constructStatic(); @@ -61,7 +61,7 @@ class DatabaseConfig static::$passwordGenerator = $passwordGenerator; } - public function getName(): ?string + public function getName(): string { return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant); } @@ -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/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index a92ccb7b..ce9219d5 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -15,25 +15,14 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; */ class DatabaseManager { - /** @var Application */ - protected $app; - - /** @var BaseDatabaseManager */ - protected $database; - - /** @var Repository */ - protected $config; - - public function __construct(Application $app, BaseDatabaseManager $database, Repository $config) - { - $this->app = $app; - $this->database = $database; - $this->config = $config; + public function __construct( + protected Application $app, + protected BaseDatabaseManager $database, + protected Repository $config, + ) { } - /** - * Connect to a tenant's database. - */ + /** Connect to a tenant's database. */ public function connectToTenant(TenantWithDatabase $tenant): void { $this->purgeTenantConnection(); @@ -41,35 +30,27 @@ class DatabaseManager $this->setDefaultConnection('tenant'); } - /** - * Reconnect to the default non-tenant connection. - */ + /** Reconnect to the default non-tenant connection. */ public function reconnectToCentral(): void { $this->purgeTenantConnection(); $this->setDefaultConnection($this->config->get('tenancy.database.central_connection')); } - /** - * Change the default database connection config. - */ + /** Change the default database connection config. */ public function setDefaultConnection(string $connection): void { $this->config['database.default'] = $connection; $this->database->setDefaultConnection($connection); } - /** - * Create the tenant database connection. - */ + /** Create the tenant database connection. */ public function createTenantConnection(TenantWithDatabase $tenant): void { $this->config['database.connections.tenant'] = $tenant->database()->connection(); } - /** - * Purge the tenant database connection. - */ + /** Purge the tenant database connection. */ public function purgeTenantConnection(): void { if (array_key_exists('tenant', $this->database->getConnections())) { @@ -83,8 +64,8 @@ class DatabaseManager * Check if a tenant can be created. * * @throws TenantCannotBeCreatedException - * @throws DatabaseManagerNotRegisteredException - * @throws TenantDatabaseAlreadyExistsException + * @throws Exceptions\DatabaseManagerNotRegisteredException + * @throws Exceptions\TenantDatabaseAlreadyExistsException */ public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void { @@ -94,8 +75,13 @@ class DatabaseManager throw new Exceptions\TenantDatabaseAlreadyExistsException($database); } - if ($manager instanceof Contracts\ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) { - throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username); + if ($manager instanceof Contracts\ManagesDatabaseUsers) { + /** @var string $username */ + $username = $tenant->database()->getUsername(); + + if ($manager->userExists($username)) { + throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username); + } } } } diff --git a/src/Database/ParentModelScope.php b/src/Database/ParentModelScope.php index 78f5de20..cfc003c1 100644 --- a/src/Database/ParentModelScope.php +++ b/src/Database/ParentModelScope.php @@ -19,7 +19,7 @@ class ParentModelScope implements Scope $builder->whereHas($builder->getModel()->getRelationshipToPrimaryModel()); } - public function extend(Builder $builder) + public function extend(Builder $builder): void { $builder->macro('withoutParentModel', function (Builder $builder) { return $builder->withoutGlobalScope($this); diff --git a/src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php b/src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php index 337864dc..f7e7440e 100644 --- a/src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php @@ -12,7 +12,8 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl { use CreatesDatabaseUsers; - public static $grants = [ + /** @var string[] */ + public static array $grants = [ 'ALTER', 'ALTER ROUTINE', 'CREATE', 'CREATE ROUTINE', 'CREATE TEMPORARY TABLES', 'CREATE VIEW', 'DELETE', 'DROP', 'EVENT', 'EXECUTE', 'INDEX', 'INSERT', 'LOCK TABLES', 'REFERENCES', 'SELECT', 'SHOW VIEW', 'TRIGGER', 'UPDATE', diff --git a/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php index fa5aa593..a7558e1b 100644 --- a/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php +++ b/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -25,11 +25,7 @@ class PostgreSQLSchemaManager extends TenantDatabaseManager public function makeConnectionConfig(array $baseConfig, string $databaseName): array { - if (version_compare(app()->version(), '9.0', '>=')) { - $baseConfig['search_path'] = $databaseName; - } else { - $baseConfig['schema'] = $databaseName; - } + $baseConfig['search_path'] = $databaseName; return $baseConfig; } diff --git a/src/Database/TenantScope.php b/src/Database/TenantScope.php index 8592f16c..fdab9d70 100644 --- a/src/Database/TenantScope.php +++ b/src/Database/TenantScope.php @@ -17,10 +17,10 @@ class TenantScope implements Scope return; } - $builder->where($model->qualifyColumn(BelongsToTenant::$tenantIdColumn), tenant()->getTenantKey()); + $builder->where($model->qualifyColumn(BelongsToTenant::tenantIdColumn()), tenant()->getTenantKey()); } - public function extend(Builder $builder) + public function extend(Builder $builder): void { $builder->macro('withoutTenancy', function (Builder $builder) { return $builder->withoutGlobalScope($this); diff --git a/src/Exceptions/RouteIsMissingTenantParameterException.php b/src/Exceptions/RouteIsMissingTenantParameterException.php index b979c819..afe56ea7 100644 --- a/src/Exceptions/RouteIsMissingTenantParameterException.php +++ b/src/Exceptions/RouteIsMissingTenantParameterException.php @@ -11,7 +11,7 @@ class RouteIsMissingTenantParameterException extends Exception { public function __construct() { - $parameter = PathTenantResolver::$tenantParameterName; + $parameter = PathTenantResolver::tenantParameterName(); parent::__construct("The route's first argument is not the tenant id (configured paramter name: $parameter)."); } diff --git a/src/Features/TenantConfig.php b/src/Features/TenantConfig.php index 50756b2c..7b82a7cd 100644 --- a/src/Features/TenantConfig.php +++ b/src/Features/TenantConfig.php @@ -16,24 +16,25 @@ use Stancl\Tenancy\Tenancy; class TenantConfig implements Feature { - /** @var Repository */ - protected $config; - public array $originalConfig = []; - public static $storageToConfigMap = [ + /** @var array */ + public static array $storageToConfigMap = [ // 'paypal_api_key' => 'services.paypal.api_key', ]; - public function __construct(Repository $config) - { - $this->config = $config; + public function __construct( + protected Repository $config, + ) { } 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 () { @@ -43,8 +44,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/Features/UniversalRoutes.php b/src/Features/UniversalRoutes.php index e327b5d3..ad0433fc 100644 --- a/src/Features/UniversalRoutes.php +++ b/src/Features/UniversalRoutes.php @@ -16,6 +16,7 @@ class UniversalRoutes implements Feature public static string $middlewareGroup = 'universal'; // todo docblock + /** @var array> */ public static array $identificationMiddlewares = [ Middleware\InitializeTenancyByDomain::class, Middleware\InitializeTenancyBySubdomain::class, @@ -42,7 +43,10 @@ class UniversalRoutes implements Feature public static function routeHasMiddleware(Route $route, string $middleware): bool { - if (in_array($middleware, $route->middleware(), true)) { + /** @var array $routeMiddleware */ + $routeMiddleware = $route->middleware(); + + if (in_array($middleware, $routeMiddleware, true)) { return true; } 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/Listeners/CreateTenantConnection.php b/src/Listeners/CreateTenantConnection.php index 01351c08..b4983d32 100644 --- a/src/Listeners/CreateTenantConnection.php +++ b/src/Listeners/CreateTenantConnection.php @@ -4,21 +4,22 @@ declare(strict_types=1); namespace Stancl\Tenancy\Listeners; +use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Events\Contracts\TenantEvent; class CreateTenantConnection { - /** @var DatabaseManager */ - protected $database; - - public function __construct(DatabaseManager $database) - { - $this->database = $database; + public function __construct( + protected DatabaseManager $database, + ) { } public function handle(TenantEvent $event): void { - $this->database->createTenantConnection($event->tenant); + /** @var TenantWithDatabase */ + $tenant = $event->tenant; + + $this->database->createTenantConnection($tenant); } } diff --git a/src/Listeners/QueueableListener.php b/src/Listeners/QueueableListener.php index e10c1e7a..f486873d 100644 --- a/src/Listeners/QueueableListener.php +++ b/src/Listeners/QueueableListener.php @@ -13,7 +13,7 @@ abstract class QueueableListener implements ShouldQueue { public static bool $shouldQueue = false; - public function shouldQueue($event): bool + public function shouldQueue(object $event): bool { if (static::$shouldQueue) { return true; diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index e88a1950..3e484f87 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Illuminate\Routing\Route; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\URL; +use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Events\InitializingTenancy; use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException; use Stancl\Tenancy\Resolvers\PathTenantResolver; @@ -33,11 +34,8 @@ class InitializeTenancyByPath extends IdentificationMiddleware // Only initialize tenancy if tenant is the first parameter // We don't want to initialize tenancy if the tenant is // simply injected into some route controller action. - if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) { - // Set tenant as a default parameter for the URLs in the current request - Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) { - URL::defaults([PathTenantResolver::$tenantParameterName => $event->tenancy->tenant->getTenantKey()]); - }); + if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) { + $this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized(); return $this->initializeTenancy( $request, @@ -50,4 +48,16 @@ class InitializeTenancyByPath extends IdentificationMiddleware return $next($request); } + + protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void + { + Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) { + /** @var Tenant $tenant */ + $tenant = $event->tenancy->tenant; + + URL::defaults([ + PathTenantResolver::tenantParameterName() => $tenant->getTenantKey(), + ]); + }); + } } diff --git a/src/Resolvers/Contracts/CachedTenantResolver.php b/src/Resolvers/Contracts/CachedTenantResolver.php index d4d5ba6e..b6a4b15c 100644 --- a/src/Resolvers/Contracts/CachedTenantResolver.php +++ b/src/Resolvers/Contracts/CachedTenantResolver.php @@ -11,23 +11,17 @@ use Stancl\Tenancy\Contracts\TenantResolver; abstract class CachedTenantResolver implements TenantResolver { - public static bool $shouldCache = false; // todo docblocks for these - - public static int $cacheTTL = 3600; // seconds - - public static string|null $cacheStore = null; // default - /** @var Repository */ protected $cache; public function __construct(Factory $cache) { - $this->cache = $cache->store(static::$cacheStore); + $this->cache = $cache->store(static::cacheStore()); } public function resolve(mixed ...$args): Tenant { - if (! static::$shouldCache) { + if (! static::shouldCache()) { return $this->resolveWithoutCache(...$args); } @@ -42,14 +36,14 @@ abstract class CachedTenantResolver implements TenantResolver } $tenant = $this->resolveWithoutCache(...$args); - $this->cache->put($key, $tenant, static::$cacheTTL); + $this->cache->put($key, $tenant, static::cacheTTL()); return $tenant; } public function invalidateCache(Tenant $tenant): void { - if (! static::$shouldCache) { + if (! static::shouldCache()) { return; } @@ -75,4 +69,19 @@ abstract class CachedTenantResolver implements TenantResolver * @return array[] */ abstract public function getArgsForTenant(Tenant $tenant): array; + + public static function shouldCache(): bool + { + return config('tenancy.identification.resolvers.' . static::class . '.cache') ?? false; + } + + public static function cacheTTL(): int + { + return config('tenancy.identification.resolvers.' . static::class . '.cache_ttl') ?? 3600; + } + + public static function cacheStore(): string|null + { + return config('tenancy.identification.resolvers.' . static::class . '.cache_store'); + } } diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index d2970bb5..cf88f579 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -14,12 +14,6 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver /** The model representing the domain that the tenant was identified on. */ public static Domain $currentDomain; // todo |null? - public static bool $shouldCache = false; - - public static int $cacheTTL = 3600; // seconds - - public static string|null $cacheStore = null; // default - public function resolveWithoutCache(mixed ...$args): Tenant { $domain = $args[0]; diff --git a/src/Resolvers/PathTenantResolver.php b/src/Resolvers/PathTenantResolver.php index c98ac37e..1359e9c1 100644 --- a/src/Resolvers/PathTenantResolver.php +++ b/src/Resolvers/PathTenantResolver.php @@ -10,21 +10,13 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException; class PathTenantResolver extends Contracts\CachedTenantResolver { - public static string $tenantParameterName = 'tenant'; - - public static bool $shouldCache = false; - - public static int $cacheTTL = 3600; // seconds - - public static string|null $cacheStore = null; // default - public function resolveWithoutCache(mixed ...$args): Tenant { /** @var Route $route */ $route = $args[0]; - if ($id = (string) $route->parameter(static::$tenantParameterName)) { - $route->forgetParameter(static::$tenantParameterName); + if ($id = (string) $route->parameter(static::tenantParameterName())) { + $route->forgetParameter(static::tenantParameterName()); if ($tenant = tenancy()->find($id)) { return $tenant; @@ -40,4 +32,9 @@ class PathTenantResolver extends Contracts\CachedTenantResolver [$tenant->getTenantKey()], ]; } + + public static function tenantParameterName(): string + { + return config('tenancy.identification.resolvers.' . static::class . '.tenant_parameter_name') ?? 'tenant'; + } } diff --git a/src/Tenancy.php b/src/Tenancy.php index 8548234c..e95e0059 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -42,7 +42,7 @@ class Tenancy } } - // todo0 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property + // 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()) { return; } @@ -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 Tenant[]|\Traversable|string[]|null $tenants + * @param array|array|\Traversable|string|int|null $tenants */ public function runForMultiple($tenants, Closure $callback): void { @@ -146,7 +157,7 @@ class Tenancy $tenants = is_string($tenants) ? [$tenants] : $tenants; // Use all tenants if $tenants is falsy - $tenants = $tenants ?: $this->model()->cursor(); // todo0 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; @@ -155,6 +166,7 @@ class Tenancy $tenant = $this->find($tenant); } + /** @var Tenant $tenant */ $this->initialize($tenant); $callback($tenant); } @@ -165,4 +177,41 @@ class Tenancy $this->end(); } } + + /** + * Cached tenant resolvers used by the package. + * + * @return array> + */ + public static function cachedResolvers(): array + { + $resolvers = config('tenancy.identification.resolvers', []); + + $cachedResolvers = array_filter($resolvers, function (array $options) { + // Resolvers based on CachedTenantResolver have the 'cache' option in the resolver config + return isset($options['cache']); + }); + + return array_keys($cachedResolvers); + } + + /** + * Tenant identification middleware used by the package. + * + * @return array> + */ + public static function middleware(): array + { + return config('tenancy.identification.middleware', []); + } + + /** + * Default tenant identification middleware used by the package. + * + * @return class-string + */ + public static function defaultMiddleware(): string + { + return config('tenancy.identification.default_middleware', Middleware\InitializeTenancyByDomain::class); + } } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 7b940300..08ea39d6 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -122,7 +122,7 @@ class TenancyServiceProvider extends ServiceProvider if ($event instanceof TenancyEvent) { match (tenancy()->logMode()) { LogMode::SILENT => tenancy()->logEvent($event), - LogMode::INSTANT => dump($event), // todo0 perhaps still log + LogMode::INSTANT => dump($event), // todo1 perhaps still log default => null, }; } diff --git a/tests/AutomaticModeTest.php b/tests/AutomaticModeTest.php index ab484ccf..fc740fc1 100644 --- a/tests/AutomaticModeTest.php +++ b/tests/AutomaticModeTest.php @@ -107,12 +107,12 @@ function contextIsSwitchedWhenTenancyInitialized() class MyBootstrapper implements TenancyBootstrapper { - public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant) + public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void { app()->instance('tenancy_initialized_for_tenant', $tenant->getTenantKey()); } - public function revert() + public function revert(): void { app()->instance('tenancy_ended', true); } diff --git a/tests/BatchTest.php b/tests/BatchTest.php index a168deb2..629a4e61 100644 --- a/tests/BatchTest.php +++ b/tests/BatchTest.php @@ -30,13 +30,13 @@ test('batch repository is set to tenant connection and reverted', function () { tenancy()->initialize($tenant); expect(getBatchRepositoryConnectionName())->toBe('tenant'); - + tenancy()->initialize($tenant2); expect(getBatchRepositoryConnectionName())->toBe('tenant'); tenancy()->end(); expect(getBatchRepositoryConnectionName())->toBe('central'); -})->skip(fn() => version_compare(app()->version(), '8.0', '<'), 'Job batches are only supported in Laravel 8+'); +}); function getBatchRepositoryConnectionName() { diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index a610fbd2..ba4ea41a 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -332,10 +332,6 @@ function getDiskPrefix(string $disk): string $disk = Storage::disk($disk); $adapter = $disk->getAdapter(); - if (! Str::startsWith(app()->version(), '9.')) { - return $adapter->getPathPrefix(); - } - $prefixer = (new ReflectionObject($adapter))->getProperty('prefixer'); $prefixer->setAccessible(true); diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php index d71375be..fa624b04 100644 --- a/tests/CachedTenantResolverTest.php +++ b/tests/CachedTenantResolverTest.php @@ -6,9 +6,7 @@ use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Tests\Etc\Tenant; -afterEach(function () { - DomainTenantResolver::$shouldCache = false; -}); +// todo@v4 test this with other resolvers as well? test('tenants can be resolved using the cached resolver', function () { $tenant = Tenant::create(); @@ -27,14 +25,14 @@ test('the underlying resolver is not touched when using the cached resolver', fu DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = false; + config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => false]); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); pest()->assertNotEmpty(DB::getQueryLog()); // not empty - DomainTenantResolver::$shouldCache = true; + config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); @@ -50,7 +48,7 @@ test('cache is invalidated when the tenant is updated', function () { DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = true; + config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); @@ -74,7 +72,7 @@ test('cache is invalidated when a tenants domain is changed', function () { DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = true; + config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php index 517fa396..32880c4f 100644 --- a/tests/PathIdentificationTest.php +++ b/tests/PathIdentificationTest.php @@ -10,8 +10,6 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Tests\Etc\Tenant; beforeEach(function () { - PathTenantResolver::$tenantParameterName = 'tenant'; - Route::group([ 'prefix' => '/{tenant}', 'middleware' => InitializeTenancyByPath::class, @@ -26,11 +24,6 @@ beforeEach(function () { }); }); -afterEach(function () { - // Global state cleanup - PathTenantResolver::$tenantParameterName = 'tenant'; -}); - test('tenant can be identified by path', function () { Tenant::create([ 'id' => 'acme', @@ -101,7 +94,7 @@ test('an exception is thrown when the routes first parameter is not tenant', fun }); test('tenant parameter name can be customized', function () { - PathTenantResolver::$tenantParameterName = 'team'; + config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_parameter_name' => 'team']); Route::group([ 'prefix' => '/{team}', diff --git a/tests/SingleDatabaseTenancyTest.php b/tests/SingleDatabaseTenancyTest.php index 8914a6d7..ec0a0edf 100644 --- a/tests/SingleDatabaseTenancyTest.php +++ b/tests/SingleDatabaseTenancyTest.php @@ -13,8 +13,6 @@ use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules; use Stancl\Tenancy\Tests\Etc\Tenant as TestTenant; beforeEach(function () { - BelongsToTenant::$tenantIdColumn = 'tenant_id'; - Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->string('text'); @@ -144,7 +142,7 @@ test('tenant id is not auto added when creating primary resources in central con }); test('tenant id column name can be customized', function () { - BelongsToTenant::$tenantIdColumn = 'team_id'; + config(['tenancy.single_db.tenant_id_column' => 'team_id']); Schema::drop('comments'); Schema::drop('posts'); diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index d43b7989..a1cd0f5b 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -6,10 +6,8 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -use Stancl\Tenancy\Controllers\TenantAssetsController; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; -use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Tests\Etc\Tenant; @@ -21,13 +19,8 @@ beforeEach(function () { Event::listen(TenancyInitialized::class, BootstrapTenancy::class); }); -afterEach(function () { - // Cleanup - TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class; -}); - test('asset can be accessed using the url returned by the tenant asset helper', function () { - TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]); $tenant = Tenant::create(); tenancy()->initialize($tenant); @@ -95,7 +88,7 @@ test('asset helper tenancy can be disabled', function () { }); test('test asset controller returns a 404 when no path is provided', function () { - TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]); $tenant = Tenant::create(); diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index d6a5b369..b16c06b6 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -154,9 +154,7 @@ test('schema manager uses schema to separate tenant dbs', function () { ]); tenancy()->initialize($tenant); - $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? - config('database.connections.' . config('database.default') . '.search_path') : - config('database.connections.' . config('database.default') . '.schema'); + $schemaConfig = config('database.connections.' . config('database.default') . '.search_path'); expect($schemaConfig)->toBe($tenant->database()->getName()); expect(config(['database.connections.pgsql.database']))->toBe($originalDatabaseName); diff --git a/tests/TestCase.php b/tests/TestCase.php index f7f8b9ad..1c0ceb83 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -103,7 +103,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase '--realpath' => true, '--force' => true, ], - 'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo0 change this to []? two tests in TenantDatabaseManagerTest are failing with that + 'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that 'queue.connections.central' => [ 'driver' => 'sync', 'central' => true,