1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 21:14:03 +00:00

Merge branch 'master' of https://github.com/archtechx/tenancy into stein-j-readied-tenant

This commit is contained in:
lukinovec 2022-10-05 13:42:38 +02:00
commit 6222a72a2f
54 changed files with 404 additions and 299 deletions

View file

@ -150,16 +150,8 @@ class TenancyServiceProvider extends ServiceProvider
protected function makeTenancyMiddlewareHighestPriority() protected function makeTenancyMiddlewareHighestPriority()
{ {
$tenancyMiddleware = [ // PreventAccessFromCentralDomains has even higher priority than the identification middleware
// Even higher priority than the initialization middleware $tenancyMiddleware = array_merge([Middleware\PreventAccessFromCentralDomains::class], config('tenancy.identification.middleware'));
Middleware\PreventAccessFromCentralDomains::class,
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
Middleware\InitializeTenancyByDomainOrSubdomain::class,
Middleware\InitializeTenancyByPath::class,
Middleware\InitializeTenancyByRequestData::class,
];
foreach (array_reverse($tenancyMiddleware) as $middleware) { foreach (array_reverse($tenancyMiddleware) as $middleware) {
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware); $this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);

View file

@ -4,6 +4,8 @@ declare(strict_types=1);
use Stancl\Tenancy\Database\Models\Domain; use Stancl\Tenancy\Database\Models\Domain;
use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Middleware;
use Stancl\Tenancy\Resolvers;
return [ return [
'tenant_model' => Tenant::class, 'tenant_model' => Tenant::class,
@ -21,6 +23,56 @@ return [
'localhost', '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. * Tenancy bootstrappers are executed when tenancy is initialized.
* Their responsibility is making Laravel features tenant-aware. * Their responsibility is making Laravel features tenant-aware.
@ -234,4 +286,12 @@ return [
'--class' => 'DatabaseSeeder', // root seeder class '--class' => 'DatabaseSeeder', // root seeder class
// '--force' => true, // '--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',
],
]; ];

View file

@ -3,7 +3,8 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Support\Facades\Route; 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', '(.*)') ->where('path', '(.*)')
->name('stancl.tenancy.asset'); ->name('stancl.tenancy.asset');

View file

@ -13,14 +13,16 @@ parameters:
- Illuminate\Database\Eloquent\Model - Illuminate\Database\Eloquent\Model
ignoreErrors: ignoreErrors:
- - '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#'
message: '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#' - '#Cannot access offset (.*?) on Illuminate\\Contracts\\Config\\Repository#'
paths:
- src/TenancyServiceProvider.php
- -
message: '#invalid type Laravel\\Telescope\\IncomingEntry#' message: '#invalid type Laravel\\Telescope\\IncomingEntry#'
paths: paths:
- src/Features/TelescopeTags.php - 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#' message: '#Parameter \#1 \$key of method Illuminate\\Contracts\\Cache\\Repository::put\(\) expects string#'
paths: paths:
@ -29,6 +31,18 @@ parameters:
message: '#PHPDoc tag \@param has invalid value \(dynamic#' message: '#PHPDoc tag \@param has invalid value \(dynamic#'
paths: paths:
- src/helpers.php - 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 checkMissingIterableValueType: false
treatPhpDocTypesAsCertain: false treatPhpDocTypesAsCertain: false

View file

@ -35,4 +35,4 @@
<log type="coverage-clover" target="coverage/phpunit/clover.xml" showUncoveredFiles="true"/> <log type="coverage-clover" target="coverage/phpunit/clover.xml" showUncoveredFiles="true"/>
<log type="coverage-html" target="coverage/phpunit/html" lowUpperBound="35" highLowerBound="70"/> <log type="coverage-html" target="coverage/phpunit/html" lowUpperBound="35" highLowerBound="70"/>
</logging> </logging>
</phpunit> </phpunit>

View file

@ -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 // Update batch repository connection to use the tenant connection
$this->previousConnection = $this->batchRepository->getConnection(); $this->previousConnection = $this->batchRepository->getConnection();
$this->batchRepository->setConnection($this->databaseManager->connection('tenant')); $this->batchRepository->setConnection($this->databaseManager->connection('tenant'));
} }
public function revert() public function revert(): void
{ {
if ($this->previousConnection) { if ($this->previousConnection) {
// Replace batch repository connection with the previously replaced one // Replace batch repository connection with the previously replaced one

View file

@ -13,18 +13,14 @@ use Stancl\Tenancy\Contracts\Tenant;
class CacheTenancyBootstrapper implements TenancyBootstrapper class CacheTenancyBootstrapper implements TenancyBootstrapper
{ {
/** @var CacheManager */ protected ?CacheManager $originalCache = null;
protected $originalCache;
/** @var Application */ public function __construct(
protected $app; protected Application $app
) {
public function __construct(Application $app)
{
$this->app = $app;
} }
public function bootstrap(Tenant $tenant) public function bootstrap(Tenant $tenant): void
{ {
$this->resetFacadeCache(); $this->resetFacadeCache();
@ -34,7 +30,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
}); });
} }
public function revert() public function revert(): void
{ {
$this->resetFacadeCache(); $this->resetFacadeCache();
@ -50,7 +46,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
* facade has been made prior to bootstrapping tenancy. The * facade has been made prior to bootstrapping tenancy. The
* facade has its own cache, separate from the container. * facade has its own cache, separate from the container.
*/ */
public function resetFacadeCache() public function resetFacadeCache(): void
{ {
Cache::clearResolvedInstances(); Cache::clearResolvedInstances();
} }

View file

@ -8,7 +8,7 @@ use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Database\DatabaseManager;
use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException; use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
class DatabaseTenancyBootstrapper implements TenancyBootstrapper class DatabaseTenancyBootstrapper implements TenancyBootstrapper
{ {
@ -20,7 +20,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
$this->database = $database; $this->database = $database;
} }
public function bootstrap(Tenant $tenant) public function bootstrap(Tenant $tenant): void
{ {
/** @var TenantWithDatabase $tenant */ /** @var TenantWithDatabase $tenant */
@ -35,7 +35,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
$this->database->connectToTenant($tenant); $this->database->connectToTenant($tenant);
} }
public function revert() public function revert(): void
{ {
$this->database->reconnectToCentral(); $this->database->reconnectToCentral();
} }

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers; namespace Stancl\Tenancy\Bootstrappers;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
@ -27,13 +28,14 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
]; ];
$this->app['url']->macro('setAssetRoot', function ($root) { $this->app['url']->macro('setAssetRoot', function ($root) {
/** @var UrlGenerator $this */
$this->assetRoot = $root; $this->assetRoot = $root;
return $this; return $this;
}); });
} }
public function bootstrap(Tenant $tenant) public function bootstrap(Tenant $tenant): void
{ {
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->getTenantKey(); $suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->getTenantKey();
@ -45,7 +47,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
// asset() // asset()
if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy'] ?? true) { if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy'] ?? true) {
if ($this->originalPaths['asset_url']) { 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']); $this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
} else { } else {
$this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => ''])); $this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => '']));
@ -82,7 +84,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
if ($url = str_replace( if ($url = str_replace(
'%tenant_id%', '%tenant_id%',
$tenant->getTenantKey(), (string) $tenant->getTenantKey(),
$this->app['config']["tenancy.filesystem.url_override.{$disk}"] ?? '' $this->app['config']["tenancy.filesystem.url_override.{$disk}"] ?? ''
)) { )) {
$this->app['config']["filesystems.disks.{$disk}.url"] = url($url); $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() // storage_path()
$this->app->useStoragePath($this->originalPaths['storage']); $this->app->useStoragePath($this->originalPaths['storage']);

View file

@ -10,27 +10,23 @@ use Stancl\Tenancy\Contracts\Tenant;
class ScoutTenancyBootstrapper implements TenancyBootstrapper class ScoutTenancyBootstrapper implements TenancyBootstrapper
{ {
/** @var Repository */ protected ?string $originalScoutPrefix = null;
protected $config;
/** @var string */ public function __construct(
protected $originalScoutPrefix; protected Repository $config,
) {
public function __construct(Repository $config)
{
$this->config = $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->originalScoutPrefix = $this->config->get('scout.prefix');
} }
$this->config->set('scout.prefix', $this->getTenantPrefix($tenant)); $this->config->set('scout.prefix', $this->getTenantPrefix($tenant));
} }
public function revert() public function revert(): void
{ {
$this->config->set('scout.prefix', $this->originalScoutPrefix); $this->config->set('scout.prefix', $this->originalScoutPrefix);
} }

View file

@ -39,7 +39,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
* However, we're registering a hook to initialize tenancy. Therefore, * However, we're registering a hook to initialize tenancy. Therefore,
* we need to register the hook at service provider execution time. * 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()); static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests());
} }
@ -52,7 +52,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
$this->setUpPayloadGenerator(); $this->setUpPayloadGenerator();
} }
protected static function setUpJobListener($dispatcher, $runningTests) protected static function setUpJobListener(Dispatcher $dispatcher, bool $runningTests): void
{ {
$previousTenant = null; $previousTenant = null;
@ -62,14 +62,11 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null); static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
}); });
if (version_compare(app()->version(), '8.64', '>=')) { $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
// JobRetryRequested only exists since Laravel 8.64 $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 // If we're running tests, we make sure to clean up after any artisan('queue:work') calls
$revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) { $revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) {
@ -82,7 +79,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails $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) { if (! $tenantId) {
// The job is not tenant-aware // The job is not tenant-aware
@ -100,7 +97,9 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
tenancy()->end(); tenancy()->end();
} }
tenancy()->initialize(tenancy()->find($tenantId)); /** @var Tenant $tenant */
$tenant = tenancy()->find($tenantId);
tenancy()->initialize($tenant);
return; return;
} }
@ -115,10 +114,13 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
// Tenancy was either not initialized, or initialized for a different tenant. // Tenancy was either not initialized, or initialized for a different tenant.
// Therefore, we initialize it for the correct 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; $tenantId = $event->job->payload()['tenant_id'] ?? null;
@ -138,7 +140,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
} }
} }
protected function setUpPayloadGenerator() protected function setUpPayloadGenerator(): void
{ {
$bootstrapper = &$this; $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) { if (! tenancy()->initialized) {
return []; return [];

View file

@ -22,18 +22,21 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
$this->config = $config; $this->config = $config;
} }
public function bootstrap(Tenant $tenant) public function bootstrap(Tenant $tenant): void
{ {
foreach ($this->prefixedConnections() as $connection) { foreach ($this->prefixedConnections() as $connection) {
$prefix = $this->config['tenancy.redis.prefix_base'] . $tenant->getTenantKey(); $prefix = $this->config['tenancy.redis.prefix_base'] . $tenant->getTenantKey();
$client = Redis::connection($connection)->client(); $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); $client->setOption($client::OPT_PREFIX, $prefix);
} }
} }
public function revert() public function revert(): void
{ {
foreach ($this->prefixedConnections() as $connection) { foreach ($this->prefixedConnections() as $connection) {
$client = Redis::connection($connection)->client(); $client = Redis::connection($connection)->client();
@ -44,7 +47,8 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
$this->originalPrefixes = []; $this->originalPrefixes = [];
} }
protected function prefixedConnections() /** @return string[] */
protected function prefixedConnections(): array
{ {
return $this->config['tenancy.redis.prefixed_connections']; return $this->config['tenancy.redis.prefixed_connections'];
} }

View file

@ -20,7 +20,7 @@ class Down extends DownCommand
protected $description = 'Put tenants into maintenance mode.'; 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 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 // the data is stored the tenant database, which means some Laravel features
@ -29,16 +29,18 @@ class Down extends DownCommand
$payload = $this->getDownDatabasePayload(); $payload = $this->getDownDatabasePayload();
// This runs for all tenants if no --tenants are specified // 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']}"); $this->line("Tenant: {$tenant['id']}");
$tenant->putDownForMaintenance($payload); $tenant->putDownForMaintenance($payload);
}); });
$this->comment('Tenants are now in maintenance mode.'); $this->comment('Tenants are now in maintenance mode.');
return 0;
} }
/** Get the payload to be placed in the "down" file. */ /** Get the payload to be placed in the "down" file. */
protected function getDownDatabasePayload() protected function getDownDatabasePayload(): array
{ {
return [ return [
'except' => $this->excludedPaths(), 'except' => $this->excludedPaths(),
@ -46,7 +48,7 @@ class Down extends DownCommand
'retry' => $this->getRetryTime(), 'retry' => $this->getRetryTime(),
'refresh' => $this->option('refresh'), 'refresh' => $this->option('refresh'),
'secret' => $this->option('secret'), 'secret' => $this->option('secret'),
'status' => (int) $this->option('status', 503), 'status' => (int) ($this->option('status') ?? 503),
]; ];
} }
} }

View file

@ -49,8 +49,8 @@ class Link extends Command
{ {
CreateStorageSymlinksAction::handle( CreateStorageSymlinksAction::handle(
$tenants, $tenants,
$this->option('relative') ?? false, (bool) ($this->option('relative') ?? false),
$this->option('force') ?? false, (bool) ($this->option('force') ?? false),
); );
$this->info('The links have been created.'); $this->info('The links have been created.');

View file

@ -13,11 +13,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
class Run extends Command class Run extends Command
{ {
use HasTenantOptions; use HasTenantOptions;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run a command for tenant(s)'; protected $description = 'Run a command for tenant(s)';
protected $signature = 'tenants:run {commandname : The artisan command.} protected $signature = 'tenants:run {commandname : The artisan command.}
@ -25,10 +21,9 @@ class Run extends Command
public function handle(): void public function handle(): void
{ {
$argvInput = $this->ArgvInput(); $argvInput = $this->argvInput();
$tenants = $this->getTenants();
tenancy()->runForMultiple($tenants, function ($tenant) use ($argvInput) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) {
$this->line("Tenant: {$tenant->getTenantKey()}"); $this->line("Tenant: {$tenant->getTenantKey()}");
$this->getLaravel() $this->getLaravel()
@ -39,12 +34,15 @@ class Run extends Command
protected function argvInput(): ArgvInput protected function argvInput(): ArgvInput
{ {
/** @var string $commandname */
$commandname = $this->argument('commandname');
// Convert string command to array // 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 // 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);
} }
} }

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Commands; namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
class TenantList extends Command class TenantList extends Command
@ -16,15 +17,16 @@ class TenantList extends Command
public function handle(): void public function handle(): void
{ {
$this->info('Listing all tenants.'); $this->info('Listing all tenants.');
tenancy()
->query() $tenants = tenancy()->query()->cursor();
->cursor()
->each(function (Tenant $tenant) { foreach ($tenants as $tenant) {
if ($tenant->domains) { /** @var Model&Tenant $tenant */
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); if ($tenant->domains) {
} else { $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? []));
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"); } else {
} $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}");
}); }
}
} }
} }

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Concerns; namespace Stancl\Tenancy\Concerns;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Contracts\Tenant;
trait DealsWithTenantSymlinks trait DealsWithTenantSymlinks
{ {
@ -23,12 +23,14 @@ trait DealsWithTenantSymlinks
$diskUrls = config('tenancy.filesystem.url_override'); $diskUrls = config('tenancy.filesystem.url_override');
$disks = config('tenancy.filesystem.root_override'); $disks = config('tenancy.filesystem.root_override');
$suffixBase = config('tenancy.filesystem.suffix_base'); $suffixBase = config('tenancy.filesystem.suffix_base');
$symlinks = collect();
$tenantKey = $tenant->getTenantKey(); $tenantKey = $tenant->getTenantKey();
/** @var Collection<array<string, string>> $symlinks */
$symlinks = collect([]);
foreach ($diskUrls as $disk => $publicPath) { foreach ($diskUrls as $disk => $publicPath) {
$storagePath = str_replace('%storage_path%', $suffixBase . $tenantKey, $disks[$disk]); $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) { tenancy()->central(function () use ($symlinks, $publicPath, $storagePath) {
$symlinks->push([public_path($publicPath) => storage_path($storagePath)]); $symlinks->push([public_path($publicPath) => storage_path($storagePath)]);

View file

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Contracts; namespace Stancl\Tenancy\Contracts;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* @property-read Tenant $tenant * @property-read Tenant $tenant
* *
@ -15,5 +17,5 @@ namespace Stancl\Tenancy\Contracts;
*/ */
interface Domain interface Domain
{ {
public function tenant(); public function tenant(): BelongsTo;
} }

View file

@ -9,7 +9,7 @@ namespace Stancl\Tenancy\Contracts;
*/ */
interface TenancyBootstrapper interface TenancyBootstrapper
{ {
public function bootstrap(Tenant $tenant); public function bootstrap(Tenant $tenant): void;
public function revert(); public function revert(): void;
} }

View file

@ -8,6 +8,7 @@ abstract class TenantCannotBeCreatedException extends \Exception
{ {
abstract public function reason(): string; abstract public function reason(): string;
/** @var string */
protected $message; protected $message;
public function __construct() public function __construct()

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Controllers;
use Illuminate\Routing\Controller;
use Stancl\Tenancy\Tenancy;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Throwable;
class TenantAssetController extends Controller // todo@docs this was renamed from TenantAssetsController
{
public function __construct()
{
$this->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);
}
}
}

View file

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Controllers;
use Closure;
use Illuminate\Routing\Controller;
use Throwable;
class TenantAssetsController extends Controller // todo rename this to TenantAssetController & update references in docs
{
public static string|array|Closure $tenancyMiddleware = Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class;
public function __construct()
{
$this->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);
}
}
}

View file

@ -12,11 +12,14 @@ use Stancl\Tenancy\Database\TenantScope;
*/ */
trait BelongsToTenant trait BelongsToTenant
{ {
public static $tenantIdColumn = 'tenant_id';
public function tenant() 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 public static function bootBelongsToTenant(): void
@ -24,9 +27,9 @@ trait BelongsToTenant
static::addGlobalScope(new TenantScope); static::addGlobalScope(new TenantScope);
static::creating(function ($model) { static::creating(function ($model) {
if (! $model->getAttribute(BelongsToTenant::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { if (! $model->getAttribute(static::tenantIdColumn()) && ! $model->relationLoaded('tenant')) {
if (tenancy()->initialized) { if (tenancy()->initialized) {
$model->setAttribute(BelongsToTenant::$tenantIdColumn, tenant()->getTenantKey()); $model->setAttribute(static::tenantIdColumn(), tenant()->getTenantKey());
$model->setRelation('tenant', tenant()); $model->setRelation('tenant', tenant());
} }
} }

View file

@ -11,11 +11,11 @@ trait HasScopedValidationRules
{ {
public function unique($table, $column = 'NULL') 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') 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());
} }
} }

View file

@ -5,21 +5,15 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns; namespace Stancl\Tenancy\Database\Concerns;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Resolvers;
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver; use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
use Stancl\Tenancy\Tenancy;
trait InvalidatesResolverCache trait InvalidatesResolverCache
{ {
public static $resolvers = [
Resolvers\DomainTenantResolver::class,
Resolvers\PathTenantResolver::class,
Resolvers\RequestDataTenantResolver::class,
];
public static function bootInvalidatesResolverCache(): void public static function bootInvalidatesResolverCache(): void
{ {
static::saved(function (Tenant $tenant) { static::saved(function (Tenant $tenant) {
foreach (static::$resolvers as $resolver) { foreach (Tenancy::cachedResolvers() as $resolver) {
/** @var CachedTenantResolver $resolver */ /** @var CachedTenantResolver $resolver */
$resolver = app($resolver); $resolver = app($resolver);

View file

@ -5,24 +5,18 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns; namespace Stancl\Tenancy\Database\Concerns;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Resolvers;
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver; use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
use Stancl\Tenancy\Tenancy;
/** /**
* Meant to be used on models that belong to tenants. * Meant to be used on models that belong to tenants.
*/ */
trait InvalidatesTenantsResolverCache trait InvalidatesTenantsResolverCache
{ {
public static $resolvers = [
Resolvers\DomainTenantResolver::class,
Resolvers\PathTenantResolver::class,
Resolvers\RequestDataTenantResolver::class,
];
public static function bootInvalidatesTenantsResolverCache(): void public static function bootInvalidatesTenantsResolverCache(): void
{ {
static::saved(function (Model $model) { static::saved(function (Model $model) {
foreach (static::$resolvers as $resolver) { foreach (Tenancy::cachedResolvers() as $resolver) {
/** @var CachedTenantResolver $resolver */ /** @var CachedTenantResolver $resolver */
$resolver = app($resolver); $resolver = app($resolver);

View file

@ -9,5 +9,15 @@ use Stancl\Tenancy\Database\DatabaseConfig;
interface TenantWithDatabase extends Tenant interface TenantWithDatabase extends Tenant
{ {
/** Get the tenant's database config. */
public function database(): DatabaseConfig; 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;
} }

View file

@ -26,20 +26,20 @@ class DatabaseConfig
public static function __constructStatic(): void public static function __constructStatic(): void
{ {
static::$usernameGenerator = static::$usernameGenerator ?? function (Tenant $tenant) { static::$usernameGenerator = static::$usernameGenerator ?? function (Model&Tenant $tenant) {
return Str::random(16); 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)); 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'); return config('tenancy.database.prefix') . $tenant->getTenantKey() . config('tenancy.database.suffix');
}; };
} }
public function __construct(Tenant $tenant) public function __construct(Model&Tenant $tenant)
{ {
static::__constructStatic(); static::__constructStatic();
@ -61,7 +61,7 @@ class DatabaseConfig
static::$passwordGenerator = $passwordGenerator; static::$passwordGenerator = $passwordGenerator;
} }
public function getName(): ?string public function getName(): string
{ {
return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant); return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant);
} }
@ -81,7 +81,7 @@ class DatabaseConfig
*/ */
public function makeCredentials(): void 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) { if ($this->manager() instanceof Contracts\ManagesDatabaseUsers) {
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant)); $this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));

View file

@ -15,25 +15,14 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
*/ */
class DatabaseManager class DatabaseManager
{ {
/** @var Application */ public function __construct(
protected $app; protected Application $app,
protected BaseDatabaseManager $database,
/** @var BaseDatabaseManager */ protected Repository $config,
protected $database; ) {
/** @var Repository */
protected $config;
public function __construct(Application $app, BaseDatabaseManager $database, Repository $config)
{
$this->app = $app;
$this->database = $database;
$this->config = $config;
} }
/** /** Connect to a tenant's database. */
* Connect to a tenant's database.
*/
public function connectToTenant(TenantWithDatabase $tenant): void public function connectToTenant(TenantWithDatabase $tenant): void
{ {
$this->purgeTenantConnection(); $this->purgeTenantConnection();
@ -41,35 +30,27 @@ class DatabaseManager
$this->setDefaultConnection('tenant'); $this->setDefaultConnection('tenant');
} }
/** /** Reconnect to the default non-tenant connection. */
* Reconnect to the default non-tenant connection.
*/
public function reconnectToCentral(): void public function reconnectToCentral(): void
{ {
$this->purgeTenantConnection(); $this->purgeTenantConnection();
$this->setDefaultConnection($this->config->get('tenancy.database.central_connection')); $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 public function setDefaultConnection(string $connection): void
{ {
$this->config['database.default'] = $connection; $this->config['database.default'] = $connection;
$this->database->setDefaultConnection($connection); $this->database->setDefaultConnection($connection);
} }
/** /** Create the tenant database connection. */
* Create the tenant database connection.
*/
public function createTenantConnection(TenantWithDatabase $tenant): void public function createTenantConnection(TenantWithDatabase $tenant): void
{ {
$this->config['database.connections.tenant'] = $tenant->database()->connection(); $this->config['database.connections.tenant'] = $tenant->database()->connection();
} }
/** /** Purge the tenant database connection. */
* Purge the tenant database connection.
*/
public function purgeTenantConnection(): void public function purgeTenantConnection(): void
{ {
if (array_key_exists('tenant', $this->database->getConnections())) { if (array_key_exists('tenant', $this->database->getConnections())) {
@ -83,8 +64,8 @@ class DatabaseManager
* Check if a tenant can be created. * Check if a tenant can be created.
* *
* @throws TenantCannotBeCreatedException * @throws TenantCannotBeCreatedException
* @throws DatabaseManagerNotRegisteredException * @throws Exceptions\DatabaseManagerNotRegisteredException
* @throws TenantDatabaseAlreadyExistsException * @throws Exceptions\TenantDatabaseAlreadyExistsException
*/ */
public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void
{ {
@ -94,8 +75,13 @@ class DatabaseManager
throw new Exceptions\TenantDatabaseAlreadyExistsException($database); throw new Exceptions\TenantDatabaseAlreadyExistsException($database);
} }
if ($manager instanceof Contracts\ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) { if ($manager instanceof Contracts\ManagesDatabaseUsers) {
throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username); /** @var string $username */
$username = $tenant->database()->getUsername();
if ($manager->userExists($username)) {
throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username);
}
} }
} }
} }

View file

@ -19,7 +19,7 @@ class ParentModelScope implements Scope
$builder->whereHas($builder->getModel()->getRelationshipToPrimaryModel()); $builder->whereHas($builder->getModel()->getRelationshipToPrimaryModel());
} }
public function extend(Builder $builder) public function extend(Builder $builder): void
{ {
$builder->macro('withoutParentModel', function (Builder $builder) { $builder->macro('withoutParentModel', function (Builder $builder) {
return $builder->withoutGlobalScope($this); return $builder->withoutGlobalScope($this);

View file

@ -12,7 +12,8 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
{ {
use CreatesDatabaseUsers; use CreatesDatabaseUsers;
public static $grants = [ /** @var string[] */
public static array $grants = [
'ALTER', 'ALTER ROUTINE', 'CREATE', 'CREATE ROUTINE', 'CREATE TEMPORARY TABLES', 'CREATE VIEW', 'ALTER', 'ALTER ROUTINE', 'CREATE', 'CREATE ROUTINE', 'CREATE TEMPORARY TABLES', 'CREATE VIEW',
'DELETE', 'DROP', 'EVENT', 'EXECUTE', 'INDEX', 'INSERT', 'LOCK TABLES', 'REFERENCES', 'SELECT', 'DELETE', 'DROP', 'EVENT', 'EXECUTE', 'INDEX', 'INSERT', 'LOCK TABLES', 'REFERENCES', 'SELECT',
'SHOW VIEW', 'TRIGGER', 'UPDATE', 'SHOW VIEW', 'TRIGGER', 'UPDATE',

View file

@ -25,11 +25,7 @@ class PostgreSQLSchemaManager extends TenantDatabaseManager
public function makeConnectionConfig(array $baseConfig, string $databaseName): array public function makeConnectionConfig(array $baseConfig, string $databaseName): array
{ {
if (version_compare(app()->version(), '9.0', '>=')) { $baseConfig['search_path'] = $databaseName;
$baseConfig['search_path'] = $databaseName;
} else {
$baseConfig['schema'] = $databaseName;
}
return $baseConfig; return $baseConfig;
} }

View file

@ -17,10 +17,10 @@ class TenantScope implements Scope
return; 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) { $builder->macro('withoutTenancy', function (Builder $builder) {
return $builder->withoutGlobalScope($this); return $builder->withoutGlobalScope($this);

View file

@ -11,7 +11,7 @@ class RouteIsMissingTenantParameterException extends Exception
{ {
public function __construct() 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)."); parent::__construct("The route's first argument is not the tenant id (configured paramter name: $parameter).");
} }

View file

@ -16,24 +16,25 @@ use Stancl\Tenancy\Tenancy;
class TenantConfig implements Feature class TenantConfig implements Feature
{ {
/** @var Repository */
protected $config;
public array $originalConfig = []; public array $originalConfig = [];
public static $storageToConfigMap = [ /** @var array<string, string|array> */
public static array $storageToConfigMap = [
// 'paypal_api_key' => 'services.paypal.api_key', // 'paypal_api_key' => 'services.paypal.api_key',
]; ];
public function __construct(Repository $config) public function __construct(
{ protected Repository $config,
$this->config = $config; ) {
} }
public function bootstrap(Tenancy $tenancy): void public function bootstrap(Tenancy $tenancy): void
{ {
Event::listen(TenancyBootstrapped::class, function (TenancyBootstrapped $event) { 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 () { Event::listen(RevertedToCentralContext::class, function () {
@ -43,8 +44,8 @@ class TenantConfig implements Feature
public function setTenantConfig(Tenant $tenant): void public function setTenantConfig(Tenant $tenant): void
{ {
/** @var Tenant|Model $tenant */
foreach (static::$storageToConfigMap as $storageKey => $configKey) { foreach (static::$storageToConfigMap as $storageKey => $configKey) {
/** @var Tenant&Model $tenant */
$override = Arr::get($tenant, $storageKey); $override = Arr::get($tenant, $storageKey);
if (! is_null($override)) { if (! is_null($override)) {

View file

@ -16,6 +16,7 @@ class UniversalRoutes implements Feature
public static string $middlewareGroup = 'universal'; public static string $middlewareGroup = 'universal';
// todo docblock // todo docblock
/** @var array<class-string<\Stancl\Tenancy\Middleware\IdentificationMiddleware>> */
public static array $identificationMiddlewares = [ public static array $identificationMiddlewares = [
Middleware\InitializeTenancyByDomain::class, Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class, Middleware\InitializeTenancyBySubdomain::class,
@ -42,7 +43,10 @@ class UniversalRoutes implements Feature
public static function routeHasMiddleware(Route $route, string $middleware): bool 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; return true;
} }

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Listeners; namespace Stancl\Tenancy\Listeners;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Events\BootstrappingTenancy; use Stancl\Tenancy\Events\BootstrappingTenancy;
use Stancl\Tenancy\Events\TenancyBootstrapped; use Stancl\Tenancy\Events\TenancyBootstrapped;
use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenancyInitialized;
@ -15,7 +16,10 @@ class BootstrapTenancy
event(new BootstrappingTenancy($event->tenancy)); event(new BootstrappingTenancy($event->tenancy));
foreach ($event->tenancy->getBootstrappers() as $bootstrapper) { 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)); event(new TenancyBootstrapped($event->tenancy));

View file

@ -4,21 +4,22 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Listeners; namespace Stancl\Tenancy\Listeners;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Database\DatabaseManager;
use Stancl\Tenancy\Events\Contracts\TenantEvent; use Stancl\Tenancy\Events\Contracts\TenantEvent;
class CreateTenantConnection class CreateTenantConnection
{ {
/** @var DatabaseManager */ public function __construct(
protected $database; protected DatabaseManager $database,
) {
public function __construct(DatabaseManager $database)
{
$this->database = $database;
} }
public function handle(TenantEvent $event): void public function handle(TenantEvent $event): void
{ {
$this->database->createTenantConnection($event->tenant); /** @var TenantWithDatabase */
$tenant = $event->tenant;
$this->database->createTenantConnection($tenant);
} }
} }

View file

@ -13,7 +13,7 @@ abstract class QueueableListener implements ShouldQueue
{ {
public static bool $shouldQueue = false; public static bool $shouldQueue = false;
public function shouldQueue($event): bool public function shouldQueue(object $event): bool
{ {
if (static::$shouldQueue) { if (static::$shouldQueue) {
return true; return true;

View file

@ -9,6 +9,7 @@ use Illuminate\Http\Request;
use Illuminate\Routing\Route; use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Events\InitializingTenancy; use Stancl\Tenancy\Events\InitializingTenancy;
use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException; use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException;
use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Resolvers\PathTenantResolver;
@ -33,11 +34,8 @@ class InitializeTenancyByPath extends IdentificationMiddleware
// Only initialize tenancy if tenant is the first parameter // Only initialize tenancy if tenant is the first parameter
// We don't want to initialize tenancy if the tenant is // We don't want to initialize tenancy if the tenant is
// simply injected into some route controller action. // simply injected into some route controller action.
if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) { if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) {
// Set tenant as a default parameter for the URLs in the current request $this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized();
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
URL::defaults([PathTenantResolver::$tenantParameterName => $event->tenancy->tenant->getTenantKey()]);
});
return $this->initializeTenancy( return $this->initializeTenancy(
$request, $request,
@ -50,4 +48,16 @@ class InitializeTenancyByPath extends IdentificationMiddleware
return $next($request); 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(),
]);
});
}
} }

View file

@ -11,23 +11,17 @@ use Stancl\Tenancy\Contracts\TenantResolver;
abstract class CachedTenantResolver implements 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 */ /** @var Repository */
protected $cache; protected $cache;
public function __construct(Factory $cache) public function __construct(Factory $cache)
{ {
$this->cache = $cache->store(static::$cacheStore); $this->cache = $cache->store(static::cacheStore());
} }
public function resolve(mixed ...$args): Tenant public function resolve(mixed ...$args): Tenant
{ {
if (! static::$shouldCache) { if (! static::shouldCache()) {
return $this->resolveWithoutCache(...$args); return $this->resolveWithoutCache(...$args);
} }
@ -42,14 +36,14 @@ abstract class CachedTenantResolver implements TenantResolver
} }
$tenant = $this->resolveWithoutCache(...$args); $tenant = $this->resolveWithoutCache(...$args);
$this->cache->put($key, $tenant, static::$cacheTTL); $this->cache->put($key, $tenant, static::cacheTTL());
return $tenant; return $tenant;
} }
public function invalidateCache(Tenant $tenant): void public function invalidateCache(Tenant $tenant): void
{ {
if (! static::$shouldCache) { if (! static::shouldCache()) {
return; return;
} }
@ -75,4 +69,19 @@ abstract class CachedTenantResolver implements TenantResolver
* @return array[] * @return array[]
*/ */
abstract public function getArgsForTenant(Tenant $tenant): 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');
}
} }

View file

@ -14,12 +14,6 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
/** The model representing the domain that the tenant was identified on. */ /** The model representing the domain that the tenant was identified on. */
public static Domain $currentDomain; // todo |null? 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 public function resolveWithoutCache(mixed ...$args): Tenant
{ {
$domain = $args[0]; $domain = $args[0];

View file

@ -10,21 +10,13 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
class PathTenantResolver extends Contracts\CachedTenantResolver 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 public function resolveWithoutCache(mixed ...$args): Tenant
{ {
/** @var Route $route */ /** @var Route $route */
$route = $args[0]; $route = $args[0];
if ($id = (string) $route->parameter(static::$tenantParameterName)) { if ($id = (string) $route->parameter(static::tenantParameterName())) {
$route->forgetParameter(static::$tenantParameterName); $route->forgetParameter(static::tenantParameterName());
if ($tenant = tenancy()->find($id)) { if ($tenant = tenancy()->find($id)) {
return $tenant; return $tenant;
@ -40,4 +32,9 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
[$tenant->getTenantKey()], [$tenant->getTenantKey()],
]; ];
} }
public static function tenantParameterName(): string
{
return config('tenancy.identification.resolvers.' . static::class . '.tenant_parameter_name') ?? 'tenant';
}
} }

View file

@ -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()) { if ($this->initialized && $this->tenant->getTenantKey() === $tenant->getTenantKey()) {
return; return;
} }
@ -99,19 +99,30 @@ class Tenancy
{ {
$class = config('tenancy.tenant_model'); $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 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. * Run a callback in the central context.
* Atomic, safely reverts to previous context. * Atomic, safely reverts to previous context.
*/ */
public function central(Closure $callback) public function central(Closure $callback): mixed
{ {
$previousTenant = $this->tenant; $previousTenant = $this->tenant;
@ -132,7 +143,7 @@ class Tenancy
* Run a callback for multiple tenants. * Run a callback for multiple tenants.
* More performant than running $tenant->run() one by one. * More performant than running $tenant->run() one by one.
* *
* @param Tenant[]|\Traversable|string[]|null $tenants * @param array<Tenant>|array<string|int>|\Traversable|string|int|null $tenants
*/ */
public function runForMultiple($tenants, Closure $callback): void public function runForMultiple($tenants, Closure $callback): void
{ {
@ -146,7 +157,7 @@ class Tenancy
$tenants = is_string($tenants) ? [$tenants] : $tenants; $tenants = is_string($tenants) ? [$tenants] : $tenants;
// Use all tenants if $tenants is falsy // 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; $originalTenant = $this->tenant;
@ -155,6 +166,7 @@ class Tenancy
$tenant = $this->find($tenant); $tenant = $this->find($tenant);
} }
/** @var Tenant $tenant */
$this->initialize($tenant); $this->initialize($tenant);
$callback($tenant); $callback($tenant);
} }
@ -165,4 +177,41 @@ class Tenancy
$this->end(); $this->end();
} }
} }
/**
* Cached tenant resolvers used by the package.
*
* @return array<class-string<Resolvers\Contracts\CachedTenantResolver>>
*/
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<class-string<Middleware\IdentificationMiddleware>>
*/
public static function middleware(): array
{
return config('tenancy.identification.middleware', []);
}
/**
* Default tenant identification middleware used by the package.
*
* @return class-string<Middleware\IdentificationMiddleware>
*/
public static function defaultMiddleware(): string
{
return config('tenancy.identification.default_middleware', Middleware\InitializeTenancyByDomain::class);
}
} }

View file

@ -122,7 +122,7 @@ class TenancyServiceProvider extends ServiceProvider
if ($event instanceof TenancyEvent) { if ($event instanceof TenancyEvent) {
match (tenancy()->logMode()) { match (tenancy()->logMode()) {
LogMode::SILENT => tenancy()->logEvent($event), LogMode::SILENT => tenancy()->logEvent($event),
LogMode::INSTANT => dump($event), // todo0 perhaps still log LogMode::INSTANT => dump($event), // todo1 perhaps still log
default => null, default => null,
}; };
} }

View file

@ -107,12 +107,12 @@ function contextIsSwitchedWhenTenancyInitialized()
class MyBootstrapper implements TenancyBootstrapper 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()); app()->instance('tenancy_initialized_for_tenant', $tenant->getTenantKey());
} }
public function revert() public function revert(): void
{ {
app()->instance('tenancy_ended', true); app()->instance('tenancy_ended', true);
} }

View file

@ -30,13 +30,13 @@ test('batch repository is set to tenant connection and reverted', function () {
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
expect(getBatchRepositoryConnectionName())->toBe('tenant'); expect(getBatchRepositoryConnectionName())->toBe('tenant');
tenancy()->initialize($tenant2); tenancy()->initialize($tenant2);
expect(getBatchRepositoryConnectionName())->toBe('tenant'); expect(getBatchRepositoryConnectionName())->toBe('tenant');
tenancy()->end(); tenancy()->end();
expect(getBatchRepositoryConnectionName())->toBe('central'); expect(getBatchRepositoryConnectionName())->toBe('central');
})->skip(fn() => version_compare(app()->version(), '8.0', '<'), 'Job batches are only supported in Laravel 8+'); });
function getBatchRepositoryConnectionName() function getBatchRepositoryConnectionName()
{ {

View file

@ -332,10 +332,6 @@ function getDiskPrefix(string $disk): string
$disk = Storage::disk($disk); $disk = Storage::disk($disk);
$adapter = $disk->getAdapter(); $adapter = $disk->getAdapter();
if (! Str::startsWith(app()->version(), '9.')) {
return $adapter->getPathPrefix();
}
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer'); $prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
$prefixer->setAccessible(true); $prefixer->setAccessible(true);

View file

@ -6,9 +6,7 @@ use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Resolvers\DomainTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
afterEach(function () { // todo@v4 test this with other resolvers as well?
DomainTenantResolver::$shouldCache = false;
});
test('tenants can be resolved using the cached resolver', function () { test('tenants can be resolved using the cached resolver', function () {
$tenant = Tenant::create(); $tenant = Tenant::create();
@ -27,14 +25,14 @@ test('the underlying resolver is not touched when using the cached resolver', fu
DB::enableQueryLog(); DB::enableQueryLog();
DomainTenantResolver::$shouldCache = false; config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => false]);
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
DB::flushQueryLog(); DB::flushQueryLog();
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
pest()->assertNotEmpty(DB::getQueryLog()); // not empty 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(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
DB::flushQueryLog(); DB::flushQueryLog();
@ -50,7 +48,7 @@ test('cache is invalidated when the tenant is updated', function () {
DB::enableQueryLog(); DB::enableQueryLog();
DomainTenantResolver::$shouldCache = true; config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
DB::flushQueryLog(); DB::flushQueryLog();
@ -74,7 +72,7 @@ test('cache is invalidated when a tenants domain is changed', function () {
DB::enableQueryLog(); DB::enableQueryLog();
DomainTenantResolver::$shouldCache = true; config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
DB::flushQueryLog(); DB::flushQueryLog();

View file

@ -10,8 +10,6 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
beforeEach(function () { beforeEach(function () {
PathTenantResolver::$tenantParameterName = 'tenant';
Route::group([ Route::group([
'prefix' => '/{tenant}', 'prefix' => '/{tenant}',
'middleware' => InitializeTenancyByPath::class, '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 () { test('tenant can be identified by path', function () {
Tenant::create([ Tenant::create([
'id' => 'acme', '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 () { test('tenant parameter name can be customized', function () {
PathTenantResolver::$tenantParameterName = 'team'; config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_parameter_name' => 'team']);
Route::group([ Route::group([
'prefix' => '/{team}', 'prefix' => '/{team}',

View file

@ -13,8 +13,6 @@ use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules;
use Stancl\Tenancy\Tests\Etc\Tenant as TestTenant; use Stancl\Tenancy\Tests\Etc\Tenant as TestTenant;
beforeEach(function () { beforeEach(function () {
BelongsToTenant::$tenantIdColumn = 'tenant_id';
Schema::create('posts', function (Blueprint $table) { Schema::create('posts', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->string('text'); $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 () { 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('comments');
Schema::drop('posts'); Schema::drop('posts');

View file

@ -6,10 +6,8 @@ use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Controllers\TenantAssetsController;
use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
@ -21,13 +19,8 @@ beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); 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 () { 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(); $tenant = Tenant::create();
tenancy()->initialize($tenant); 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 () { 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(); $tenant = Tenant::create();

View file

@ -154,9 +154,7 @@ test('schema manager uses schema to separate tenant dbs', function () {
]); ]);
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
$schemaConfig = version_compare(app()->version(), '9.0', '>=') ? $schemaConfig = config('database.connections.' . config('database.default') . '.search_path');
config('database.connections.' . config('database.default') . '.search_path') :
config('database.connections.' . config('database.default') . '.schema');
expect($schemaConfig)->toBe($tenant->database()->getName()); expect($schemaConfig)->toBe($tenant->database()->getName());
expect(config(['database.connections.pgsql.database']))->toBe($originalDatabaseName); expect(config(['database.connections.pgsql.database']))->toBe($originalDatabaseName);

View file

@ -103,7 +103,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'--realpath' => true, '--realpath' => true,
'--force' => 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' => [ 'queue.connections.central' => [
'driver' => 'sync', 'driver' => 'sync',
'central' => true, 'central' => true,