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

Merge branch 'master' of https://github.com/archtechx/tenancy into add-skip-failing-options-to-migrate

This commit is contained in:
lukinovec 2022-10-05 13:55:07 +02:00
commit b76ed4ad08
94 changed files with 752 additions and 567 deletions

View file

@ -10,6 +10,7 @@ $rules = [
'operators' => [ 'operators' => [
'=>' => null, '=>' => null,
'|' => 'no_space', '|' => 'no_space',
'&' => 'no_space',
] ]
], ],
'blank_line_after_namespace' => true, 'blank_line_after_namespace' => true,

View file

@ -144,16 +144,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.
@ -215,4 +267,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

@ -63,6 +63,7 @@
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml", "docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
"coverage": "open coverage/phpunit/html/index.html", "coverage": "open coverage/phpunit/html/index.html",
"phpstan": "vendor/bin/phpstan", "phpstan": "vendor/bin/phpstan",
"cs": "php-cs-fixer fix --config=.php-cs-fixer.php",
"test": "PHP_VERSION=8.1 ./test --no-coverage", "test": "PHP_VERSION=8.1 ./test --no-coverage",
"test-full": "PHP_VERSION=8.1 ./test" "test-full": "PHP_VERSION=8.1 ./test"
}, },

View file

@ -10,16 +10,39 @@ parameters:
universalObjectCratesClasses: universalObjectCratesClasses:
- Illuminate\Routing\Route - Illuminate\Routing\Route
- 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#'
paths:
- src/helpers.php
-
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 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

@ -8,7 +8,7 @@ use Exception;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection; use Illuminate\Support\LazyCollection;
use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks; use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks;
use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Events\CreatingStorageSymlink; use Stancl\Tenancy\Events\CreatingStorageSymlink;
use Stancl\Tenancy\Events\StorageSymlinkCreated; use Stancl\Tenancy\Events\StorageSymlinkCreated;

View file

@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Actions;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection; use Illuminate\Support\LazyCollection;
use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks; use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks;
use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Events\RemovingStorageSymlink; use Stancl\Tenancy\Events\RemovingStorageSymlink;
use Stancl\Tenancy\Events\StorageSymlinkRemoved; use Stancl\Tenancy\Events\StorageSymlinkRemoved;

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

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

54
src/Commands/Down.php Normal file
View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Commands;
use Illuminate\Foundation\Console\DownCommand;
use Stancl\Tenancy\Concerns\HasATenantsOption;
class Down extends DownCommand
{
use HasATenantsOption;
protected $signature = 'tenants:down
{--redirect= : The path that users should be redirected to}
{--retry= : The number of seconds after which the request may be retried}
{--refresh= : The number of seconds after which the browser may refresh}
{--secret= : The secret phrase that may be used to bypass maintenance mode}
{--status=503 : The status code that should be used when returning the maintenance mode response}';
protected $description = 'Put tenants into maintenance mode.';
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
// are not available with tenants.
$payload = $this->getDownDatabasePayload();
// This runs for all tenants if no --tenants are specified
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(): array
{
return [
'except' => $this->excludedPaths(),
'redirect' => $this->redirectPath(),
'retry' => $this->getRetryTime(),
'refresh' => $this->option('refresh'),
'secret' => $this->option('secret'),
'status' => (int) ($this->option('status') ?? 503),
];
}
}

View file

@ -8,24 +8,11 @@ use Illuminate\Console\Command;
class Install extends Command class Install extends Command
{ {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tenancy:install'; protected $signature = 'tenancy:install';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Install stancl/tenancy.'; protected $description = 'Install stancl/tenancy.';
/** public function handle(): void
* Execute the console command.
*/
public function handle()
{ {
$this->comment('Installing stancl/tenancy...'); $this->comment('Installing stancl/tenancy...');
$this->callSilent('vendor:publish', [ $this->callSilent('vendor:publish', [

View file

@ -15,30 +15,15 @@ class Link extends Command
{ {
use HasATenantsOption; use HasATenantsOption;
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'tenants:link protected $signature = 'tenants:link
{--tenants=* : The tenant(s) to run the command for. Default: all} {--tenants=* : The tenant(s) to run the command for. Default: all}
{--relative : Create the symbolic link using relative paths} {--relative : Create the symbolic link using relative paths}
{--force : Recreate existing symbolic links} {--force : Recreate existing symbolic links}
{--remove : Remove symbolic links}'; {--remove : Remove symbolic links}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create or remove tenant symbolic links.'; protected $description = 'Create or remove tenant symbolic links.';
/** public function handle(): void
* Execute the console command.
*
* @return void
*/
public function handle()
{ {
$tenants = $this->getTenants(); $tenants = $this->getTenants();
@ -64,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

@ -17,7 +17,7 @@ use Stancl\Tenancy\Events\MigratingDatabase;
class Migrate extends MigrateCommand class Migrate extends MigrateCommand
{ {
use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand; use HasATenantsOption, ExtendsLaravelCommand;
protected $description = 'Run migrations for tenant(s)'; protected $description = 'Run migrations for tenant(s)';
@ -35,10 +35,7 @@ class Migrate extends MigrateCommand
$this->specifyParameters(); $this->specifyParameters();
} }
/** public function handle(): int
* Execute the console command.
*/
public function handle()
{ {
foreach (config('tenancy.migration_parameters') as $parameter => $value) { foreach (config('tenancy.migration_parameters') as $parameter => $value) {
if (! $this->input->hasParameterOption($parameter)) { if (! $this->input->hasParameterOption($parameter)) {
@ -47,7 +44,7 @@ class Migrate extends MigrateCommand
} }
if (! $this->confirmToProceed()) { if (! $this->confirmToProceed()) {
return; return 1;
} }
try { try {

View file

@ -5,19 +5,13 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Commands; namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Stancl\Tenancy\Concerns\DealsWithMigrations;
use Stancl\Tenancy\Concerns\HasATenantsOption; use Stancl\Tenancy\Concerns\HasATenantsOption;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
final class MigrateFresh extends Command final class MigrateFresh extends Command
{ {
use HasATenantsOption, DealsWithMigrations; use HasATenantsOption;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Drop all tables and re-run all migrations for tenant(s)'; protected $description = 'Drop all tables and re-run all migrations for tenant(s)';
public function __construct() public function __construct()
@ -29,12 +23,9 @@ final class MigrateFresh extends Command
$this->setName('tenants:migrate-fresh'); $this->setName('tenants:migrate-fresh');
} }
/** public function handle(): void
* Execute the console command.
*/
public function handle()
{ {
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->info('Dropping tables.'); $this->info('Dropping tables.');
$this->call('db:wipe', array_filter([ $this->call('db:wipe', array_filter([
'--database' => 'tenant', '--database' => 'tenant',

View file

@ -6,7 +6,6 @@ namespace Stancl\Tenancy\Commands;
use Illuminate\Database\Console\Migrations\RollbackCommand; use Illuminate\Database\Console\Migrations\RollbackCommand;
use Illuminate\Database\Migrations\Migrator; use Illuminate\Database\Migrations\Migrator;
use Stancl\Tenancy\Concerns\DealsWithMigrations;
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
use Stancl\Tenancy\Concerns\HasATenantsOption; use Stancl\Tenancy\Concerns\HasATenantsOption;
use Stancl\Tenancy\Events\DatabaseRolledBack; use Stancl\Tenancy\Events\DatabaseRolledBack;
@ -14,25 +13,10 @@ use Stancl\Tenancy\Events\RollingBackDatabase;
class Rollback extends RollbackCommand class Rollback extends RollbackCommand
{ {
use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand; use HasATenantsOption, ExtendsLaravelCommand;
protected static function getTenantCommandName(): string
{
return 'tenants:rollback';
}
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rollback migrations for tenant(s).'; protected $description = 'Rollback migrations for tenant(s).';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(Migrator $migrator) public function __construct(Migrator $migrator)
{ {
parent::__construct($migrator); parent::__construct($migrator);
@ -40,10 +24,7 @@ class Rollback extends RollbackCommand
$this->specifyTenantSignature(); $this->specifyTenantSignature();
} }
/** public function handle(): int
* Execute the console command.
*/
public function handle()
{ {
foreach (config('tenancy.migration_parameters') as $parameter => $value) { foreach (config('tenancy.migration_parameters') as $parameter => $value) {
if (! $this->input->hasParameterOption($parameter)) { if (! $this->input->hasParameterOption($parameter)) {
@ -52,10 +33,10 @@ class Rollback extends RollbackCommand
} }
if (! $this->confirmToProceed()) { if (! $this->confirmToProceed()) {
return; return 1;
} }
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->line("Tenant: {$tenant->getTenantKey()}"); $this->line("Tenant: {$tenant->getTenantKey()}");
event(new RollingBackDatabase($tenant)); event(new RollingBackDatabase($tenant));
@ -65,5 +46,12 @@ class Rollback extends RollbackCommand
event(new DatabaseRolledBack($tenant)); event(new DatabaseRolledBack($tenant));
}); });
return 0;
}
protected static function getTenantCommandName(): string
{
return 'tenants:rollback';
} }
} }

View file

@ -6,33 +6,24 @@ namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel; use Illuminate\Contracts\Console\Kernel;
use Stancl\Tenancy\Concerns\HasATenantsOption;
use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutput;
class Run extends Command class Run extends Command
{ {
/** use HasATenantsOption;
* The console command description.
*
* @var string
*/
protected $description = 'Run a command for tenant(s)'; protected $description = 'Run a command for tenant(s)';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tenants:run {commandname : The artisan command.} protected $signature = 'tenants:run {commandname : The artisan command.}
{--tenants=* : The tenant(s) to run the command for. Default: all}'; {--tenants=* : The tenant(s) to run the command for. Default: all}';
/** public function handle(): void
* Execute the console command.
*/
public function handle()
{ {
$argvInput = $this->ArgvInput(); $argvInput = $this->argvInput();
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) use ($argvInput) {
tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) {
$this->line("Tenant: {$tenant->getTenantKey()}"); $this->line("Tenant: {$tenant->getTenantKey()}");
$this->getLaravel() $this->getLaravel()
@ -41,17 +32,17 @@ class Run extends Command
}); });
} }
/** protected function argvInput(): ArgvInput
* Get command as ArgvInput instance.
*/
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

@ -14,29 +14,16 @@ class Seed extends SeedCommand
{ {
use HasATenantsOption; use HasATenantsOption;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Seed tenant database(s).'; protected $description = 'Seed tenant database(s).';
protected $name = 'tenants:seed'; protected $name = 'tenants:seed';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(ConnectionResolverInterface $resolver) public function __construct(ConnectionResolverInterface $resolver)
{ {
parent::__construct($resolver); parent::__construct($resolver);
} }
/** public function handle(): int
* Execute the console command.
*/
public function handle()
{ {
foreach (config('tenancy.seeder_parameters') as $parameter => $value) { foreach (config('tenancy.seeder_parameters') as $parameter => $value) {
if (! $this->input->hasParameterOption($parameter)) { if (! $this->input->hasParameterOption($parameter)) {
@ -45,10 +32,10 @@ class Seed extends SeedCommand
} }
if (! $this->confirmToProceed()) { if (! $this->confirmToProceed()) {
return; return 1;
} }
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->line("Tenant: {$tenant->getTenantKey()}"); $this->line("Tenant: {$tenant->getTenantKey()}");
event(new SeedingDatabase($tenant)); event(new SeedingDatabase($tenant));
@ -58,5 +45,7 @@ class Seed extends SeedCommand
event(new DatabaseSeeded($tenant)); event(new DatabaseSeeded($tenant));
}); });
return 0;
} }
} }

View file

@ -5,39 +5,28 @@ 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
{ {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tenants:list'; protected $signature = 'tenants:list';
/**
* The console command description.
*
* @var string
*/
protected $description = 'List tenants.'; protected $description = 'List tenants.';
/** public function handle(): void
* Execute the console command.
*/
public function handle()
{ {
$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()}");
}); }
}
} }
} }

27
src/Commands/Up.php Normal file
View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command;
use Stancl\Tenancy\Concerns\HasATenantsOption;
class Up extends Command
{
use HasATenantsOption;
protected $signature = 'tenants:up';
protected $description = 'Put tenants out of maintenance mode.';
public function handle(): void
{
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->line("Tenant: {$tenant['id']}");
$tenant->bringUpFromMaintenance();
});
$this->comment('Tenants are now out of maintenance mode.');
}
}

View file

@ -6,12 +6,12 @@ namespace Stancl\Tenancy\Concerns;
trait DealsWithMigrations trait DealsWithMigrations
{ {
protected function getMigrationPaths() protected function getMigrationPaths(): array
{ {
if ($this->input->hasOption('path') && $this->input->getOption('path')) { if ($this->input->hasOption('path') && $this->input->getOption('path')) {
return parent::getMigrationPaths(); return parent::getMigrationPaths();
} }
return database_path('migrations/tenant'); return [database_path('migrations/tenant')];
} }
} }

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
{ {
@ -15,25 +15,29 @@ trait DealsWithTenantSymlinks
* Tenants can have a symlink for each disk registered in the tenancy.filesystem.url_override config. * Tenants can have a symlink for each disk registered in the tenancy.filesystem.url_override config.
* *
* This is used for creating all possible tenant symlinks and removing all existing tenant symlinks. * This is used for creating all possible tenant symlinks and removing all existing tenant symlinks.
*
* @return Collection<string, string>
*/ */
protected static function possibleTenantSymlinks(Tenant $tenant): Collection protected static function possibleTenantSymlinks(Tenant $tenant): Collection
{ {
$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)]);
}); });
} }
return $symlinks->mapWithKeys(fn ($item) => $item); return $symlinks->mapWithKeys(fn ($item) => $item); // [[a => b], [c => d]] -> [a => b, c => d]
} }
/** Determine if the provided path is an existing symlink. */ /** Determine if the provided path is an existing symlink. */

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

@ -4,10 +4,12 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Contracts; namespace Stancl\Tenancy\Contracts;
use Illuminate\Database\Eloquent\Model;
interface UniqueIdentifierGenerator interface UniqueIdentifierGenerator
{ {
/** /**
* Generate a unique identifier. * Generate a unique identifier for a model.
*/ */
public static function generate($resource): string; public static function generate(Model $model): string;
} }

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
{
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

@ -10,6 +10,8 @@ use Stancl\Tenancy\Contracts\Domain;
/** /**
* @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains * @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains
* @mixin \Illuminate\Database\Eloquent\Model
* @mixin \Stancl\Tenancy\Contracts\Tenant
*/ */
trait HasDomains trait HasDomains
{ {

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

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
/**
* @mixin \Stancl\Tenancy\Contracts\Tenant
*/
trait InitializationHelpers
{
public function enter(): void
{
tenancy()->initialize($this);
}
public function leave(): void
{
tenancy()->end();
}
}

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

@ -4,17 +4,27 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns; namespace Stancl\Tenancy\Database\Concerns;
use Carbon\Carbon; /**
* @mixin \Illuminate\Database\Eloquent\Model
*/
trait MaintenanceMode trait MaintenanceMode
{ {
public function putDownForMaintenance($data = []) public function putDownForMaintenance($data = []): void
{ {
$this->update(['maintenance_mode' => [ $this->update([
'time' => $data['time'] ?? Carbon::now()->getTimestamp(), 'maintenance_mode' => [
'message' => $data['message'] ?? null, 'except' => $data['except'] ?? null,
'retry' => $data['retry'] ?? null, 'redirect' => $data['redirect'] ?? null,
'allowed' => $data['allowed'] ?? [], 'retry' => $data['retry'] ?? null,
]]); 'refresh' => $data['refresh'] ?? null,
'secret' => $data['secret'] ?? null,
'status' => $data['status'] ?? 503,
],
]);
}
public function bringUpFromMaintenance(): void
{
$this->update(['maintenance_mode' => null]);
} }
} }

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

@ -26,6 +26,7 @@ class Tenant extends Model implements Contracts\Tenant
Concerns\HasDataColumn, Concerns\HasDataColumn,
Concerns\HasInternalKeys, Concerns\HasInternalKeys,
Concerns\TenantRun, Concerns\TenantRun,
Concerns\InitializationHelpers,
Concerns\InvalidatesResolverCache; Concerns\InvalidatesResolverCache;
protected $table = 'tenants'; protected $table = 'tenants';

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

@ -8,11 +8,8 @@ use Stancl\Tenancy\Tenancy;
abstract class TenancyEvent abstract class TenancyEvent
{ {
/** @var Tenancy */ public function __construct(
public $tenancy; public Tenancy $tenancy,
) {
public function __construct(Tenancy $tenancy)
{
$this->tenancy = $tenancy;
} }
} }

View file

@ -8,7 +8,7 @@ use Exception;
class DomainOccupiedByOtherTenantException extends Exception class DomainOccupiedByOtherTenantException extends Exception
{ {
public function __construct($domain) public function __construct(string $domain)
{ {
parent::__construct("The $domain domain is occupied by another tenant."); parent::__construct("The $domain domain is occupied by another tenant.");
} }

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

@ -8,7 +8,7 @@ use Exception;
class TenancyNotInitializedException extends Exception class TenancyNotInitializedException extends Exception
{ {
public function __construct($message = '') public function __construct(string $message = '')
{ {
parent::__construct($message ?: 'Tenancy is not initialized.'); parent::__construct($message ?: 'Tenancy is not initialized.');
} }

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

@ -24,7 +24,7 @@ class CreateDatabase implements ShouldQueue
) { ) {
} }
public function handle(DatabaseManager $databaseManager) public function handle(DatabaseManager $databaseManager): bool
{ {
event(new CreatingDatabase($this->tenant)); event(new CreatingDatabase($this->tenant));
@ -38,5 +38,7 @@ class CreateDatabase implements ShouldQueue
$this->tenant->database()->manager()->createDatabase($this->tenant); $this->tenant->database()->manager()->createDatabase($this->tenant);
event(new DatabaseCreated($this->tenant)); event(new DatabaseCreated($this->tenant));
return true;
} }
} }

View file

@ -16,24 +16,12 @@ class CreateStorageSymlinks implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Tenant $tenant; public function __construct(
public Tenant $tenant,
/** ) {
* Create a new job instance.
*
* @return void
*/
public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
} }
/** public function handle(): void
* Execute the job.
*
* @return void
*/
public function handle()
{ {
CreateStorageSymlinksAction::handle($this->tenant); CreateStorageSymlinksAction::handle($this->tenant);
} }

View file

@ -9,14 +9,12 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
class DeleteDomains class DeleteDomains
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var TenantWithDatabase&Model&HasDomains */ // todo unresolvable type for phpstan
protected TenantWithDatabase&Model $tenant; protected TenantWithDatabase&Model $tenant;
public function __construct(TenantWithDatabase&Model $tenant) public function __construct(TenantWithDatabase&Model $tenant)

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

@ -7,7 +7,6 @@ namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
@ -21,19 +20,38 @@ class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
if (tenant('maintenance_mode')) { if (tenant('maintenance_mode')) {
$data = tenant('maintenance_mode'); $data = tenant('maintenance_mode');
if (isset($data['allowed']) && IpUtils::checkIp($request->ip(), (array) $data['allowed'])) { if (isset($data['secret']) && $request->path() === $data['secret']) {
return $this->bypassResponse($data['secret']);
}
if ($this->hasValidBypassCookie($request, $data) ||
$this->inExceptArray($request)) {
return $next($request); return $next($request);
} }
if ($this->inExceptArray($request)) { if (isset($data['redirect'])) {
return $next($request); $path = $data['redirect'] === '/'
? $data['redirect']
: trim($data['redirect'], '/');
if ($request->path() !== $path) {
return redirect($path);
}
}
if (isset($data['template'])) {
return response(
$data['template'],
(int) ($data['status'] ?? 503),
$this->getHeaders($data)
);
} }
throw new HttpException( throw new HttpException(
503, (int) ($data['status'] ?? 503),
'Service Unavailable', 'Service Unavailable',
null, null,
isset($data['retry']) ? ['Retry-After' => $data['retry']] : [] $this->getHeaders($data)
); );
} }

View file

@ -4,22 +4,22 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Middleware; namespace Stancl\Tenancy\Middleware;
use Closure;
use Illuminate\Http\Request;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Contracts\TenantResolver; use Stancl\Tenancy\Contracts\TenantResolver;
use Stancl\Tenancy\Tenancy; use Stancl\Tenancy\Tenancy;
/**
* @property Tenancy $tenancy
* @property TenantResolver $resolver
*/
abstract class IdentificationMiddleware abstract class IdentificationMiddleware
{ {
/** @var callable */ public static ?Closure $onFail = null;
public static $onFail;
/** @var Tenancy */ /** @return \Illuminate\Http\Response|mixed */
protected $tenancy; public function initializeTenancy(Request $request, Closure $next, mixed ...$resolverArguments): mixed
/** @var TenantResolver */
protected $resolver;
public function initializeTenancy($request, $next, ...$resolverArguments)
{ {
try { try {
$this->tenancy->initialize( $this->tenancy->initialize(

View file

@ -5,32 +5,22 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Middleware; namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request;
use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Resolvers\DomainTenantResolver;
use Stancl\Tenancy\Tenancy; use Stancl\Tenancy\Tenancy;
class InitializeTenancyByDomain extends IdentificationMiddleware class InitializeTenancyByDomain extends IdentificationMiddleware
{ {
/** @var callable|null */ public static ?Closure $onFail = null;
public static $onFail;
/** @var Tenancy */ public function __construct(
protected $tenancy; protected Tenancy $tenancy,
protected DomainTenantResolver $resolver,
/** @var DomainTenantResolver */ ) {
protected $resolver;
public function __construct(Tenancy $tenancy, DomainTenantResolver $resolver)
{
$this->tenancy = $tenancy;
$this->resolver = $resolver;
} }
/** /** @return \Illuminate\Http\Response|mixed */
* Handle an incoming request. public function handle(Request $request, Closure $next): mixed
*
* @param \Illuminate\Http\Request $request
*/
public function handle($request, Closure $next)
{ {
return $this->initializeTenancy( return $this->initializeTenancy(
$request, $request,

View file

@ -5,16 +5,13 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Middleware; namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class InitializeTenancyByDomainOrSubdomain class InitializeTenancyByDomainOrSubdomain
{ {
/** /** @return \Illuminate\Http\Response|mixed */
* Handle an incoming request. public function handle(Request $request, Closure $next): mixed
*
* @param \Illuminate\Http\Request $request
*/
public function handle($request, Closure $next)
{ {
if ($this->isSubdomain($request->getHost())) { if ($this->isSubdomain($request->getHost())) {
return app(InitializeTenancyBySubdomain::class)->handle($request, $next); return app(InitializeTenancyBySubdomain::class)->handle($request, $next);

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;
@ -16,22 +17,16 @@ use Stancl\Tenancy\Tenancy;
class InitializeTenancyByPath extends IdentificationMiddleware class InitializeTenancyByPath extends IdentificationMiddleware
{ {
/** @var callable|null */ public static ?Closure $onFail = null;
public static $onFail;
/** @var Tenancy */ public function __construct(
protected $tenancy; protected Tenancy $tenancy,
protected PathTenantResolver $resolver,
/** @var PathTenantResolver */ ) {
protected $resolver;
public function __construct(Tenancy $tenancy, PathTenantResolver $resolver)
{
$this->tenancy = $tenancy;
$this->resolver = $resolver;
} }
public function handle(Request $request, Closure $next) /** @return \Illuminate\Http\Response|mixed */
public function handle(Request $request, Closure $next): mixed
{ {
/** @var Route $route */ /** @var Route $route */
$route = $request->route(); $route = $request->route();
@ -39,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,
@ -56,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,33 +11,18 @@ use Stancl\Tenancy\Tenancy;
class InitializeTenancyByRequestData extends IdentificationMiddleware class InitializeTenancyByRequestData extends IdentificationMiddleware
{ {
/** @var string|null */ public static string $header = 'X-Tenant';
public static $header = 'X-Tenant'; public static string $queryParameter = 'tenant';
public static ?Closure $onFail = null;
/** @var string|null */ public function __construct(
public static $queryParameter = 'tenant'; protected Tenancy $tenancy,
protected RequestDataTenantResolver $resolver,
/** @var callable|null */ ) {
public static $onFail;
/** @var Tenancy */
protected $tenancy;
/** @var TenantResolver */
protected $resolver;
public function __construct(Tenancy $tenancy, RequestDataTenantResolver $resolver)
{
$this->tenancy = $tenancy;
$this->resolver = $resolver;
} }
/** /** @return \Illuminate\Http\Response|mixed */
* Handle an incoming request. public function handle(Request $request, Closure $next): mixed
*
* @param \Illuminate\Http\Request $request
*/
public function handle($request, Closure $next)
{ {
if ($request->method() !== 'OPTIONS') { if ($request->method() !== 'OPTIONS') {
return $this->initializeTenancy($request, $next, $this->getPayload($request)); return $this->initializeTenancy($request, $next, $this->getPayload($request));

View file

@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
use Exception; use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Stancl\Tenancy\Exceptions\NotASubdomainException; use Stancl\Tenancy\Exceptions\NotASubdomainException;
@ -21,15 +22,10 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
*/ */
public static $subdomainIndex = 0; public static $subdomainIndex = 0;
/** @var callable|null */ public static ?Closure $onFail = null;
public static $onFail;
/** /** @return Response|mixed */
* Handle an incoming request. public function handle(Request $request, Closure $next): mixed
*
* @param \Illuminate\Http\Request $request
*/
public function handle($request, Closure $next)
{ {
$subdomain = $this->makeSubdomain($request->getHost()); $subdomain = $this->makeSubdomain($request->getHost());

View file

@ -11,12 +11,11 @@ class PreventAccessFromCentralDomains
{ {
/** /**
* Set this property if you want to customize the on-fail behavior. * Set this property if you want to customize the on-fail behavior.
*
* @var callable|null
*/ */
public static $abortRequest; public static ?Closure $abortRequest;
public function handle(Request $request, Closure $next) /** @return \Illuminate\Http\Response|mixed */
public function handle(Request $request, Closure $next): mixed
{ {
if (in_array($request->getHost(), config('tenancy.central_domains'))) { if (in_array($request->getHost(), config('tenancy.central_domains'))) {
$abortRequest = static::$abortRequest ?? function () { $abortRequest = static::$abortRequest ?? function () {

View file

@ -10,9 +10,10 @@ use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
class ScopeSessions class ScopeSessions
{ {
public static $tenantIdKey = '_tenant_id'; public static string $tenantIdKey = '_tenant_id';
public function handle(Request $request, Closure $next) /** @return \Illuminate\Http\Response|mixed */
public function handle(Request $request, Closure $next): mixed
{ {
if (! tenancy()->initialized) { if (! tenancy()->initialized) {
throw new TenancyNotInitializedException('Tenancy needs to be initialized before the session scoping middleware is executed'); throw new TenancyNotInitializedException('Tenancy needs to be initialized before the session scoping middleware is executed');

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;
} }
@ -65,7 +59,7 @@ abstract class CachedTenantResolver implements TenantResolver
abstract public function resolveWithoutCache(mixed ...$args): Tenant; abstract public function resolveWithoutCache(mixed ...$args): Tenant;
public function resolved(Tenant $tenant, ...$args): void public function resolved(Tenant $tenant, mixed ...$args): void
{ {
} }
@ -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,17 +14,10 @@ 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];
/** @var Tenant|null $tenant */
$tenant = config('tenancy.tenant_model')::query() $tenant = config('tenancy.tenant_model')::query()
->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain)) ->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain))
->with('domains') ->with('domains')
@ -39,7 +32,7 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
throw new TenantCouldNotBeIdentifiedOnDomainException($args[0]); throw new TenantCouldNotBeIdentifiedOnDomainException($args[0]);
} }
public function resolved(Tenant $tenant, ...$args): void public function resolved(Tenant $tenant, mixed ...$args): void
{ {
$this->setCurrentDomain($tenant, $args[0]); $this->setCurrentDomain($tenant, $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 = $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;
@ -37,7 +29,12 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
public function getArgsForTenant(Tenant $tenant): array public function getArgsForTenant(Tenant $tenant): array
{ {
return [ return [
[$tenant->id], [$tenant->getTenantKey()],
]; ];
} }
public static function tenantParameterName(): string
{
return config('tenancy.identification.resolvers.' . static::class . '.tenant_parameter_name') ?? 'tenant';
}
} }

View file

@ -17,7 +17,7 @@ class RequestDataTenantResolver extends Contracts\CachedTenantResolver
public function resolveWithoutCache(mixed ...$args): Tenant public function resolveWithoutCache(mixed ...$args): Tenant
{ {
$payload = $args[0]; $payload = (string) $args[0];
if ($payload && $tenant = tenancy()->find($payload)) { if ($payload && $tenant = tenancy()->find($payload)) {
return $tenant; return $tenant;
@ -29,7 +29,7 @@ class RequestDataTenantResolver extends Contracts\CachedTenantResolver
public function getArgsForTenant(Tenant $tenant): array public function getArgsForTenant(Tenant $tenant): array
{ {
return [ return [
[$tenant->id], [$tenant->getTenantKey()],
]; ];
} }
} }

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 falsey // Use all tenants if $tenants is falsey
$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

@ -86,6 +86,8 @@ class TenancyServiceProvider extends ServiceProvider
Commands\TenantList::class, Commands\TenantList::class,
Commands\TenantDump::class, Commands\TenantDump::class,
Commands\MigrateFresh::class, Commands\MigrateFresh::class,
Commands\Down::class,
Commands\Up::class,
]); ]);
$this->publishes([ $this->publishes([
@ -118,7 +120,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

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy; namespace Stancl\Tenancy;
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
@ -11,7 +12,7 @@ use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
class UUIDGenerator implements UniqueIdentifierGenerator class UUIDGenerator implements UniqueIdentifierGenerator
{ {
public static function generate($resource): string public static function generate(Model $model): string
{ {
return Uuid::uuid4()->toString(); return Uuid::uuid4()->toString();
} }

View file

@ -35,6 +35,7 @@ if (! function_exists('tenant')) {
if (! function_exists('tenant_asset')) { if (! function_exists('tenant_asset')) {
// todo docblock // todo docblock
// todo add an option to generate paths respecting the ASSET_URL
function tenant_asset(string|null $asset): string function tenant_asset(string|null $asset): string
{ {
return route('stancl.tenancy.asset', ['path' => $asset]); return route('stancl.tenancy.asset', ['path' => $asset]);
@ -42,16 +43,42 @@ if (! function_exists('tenant_asset')) {
} }
if (! function_exists('global_asset')) { if (! function_exists('global_asset')) {
function global_asset(string $asset) // todo types, also inside the globalUrl implementation function global_asset(string $asset): string
{ {
return app('globalUrl')->asset($asset); return app('globalUrl')->asset($asset);
} }
} }
if (! function_exists('global_cache')) { if (! function_exists('global_cache')) {
function global_cache() /**
* Get / set the specified cache value in the global cache store.
*
* If an array is passed, we'll assume you want to put to the cache.
*
* @param dynamic key|key,default|data,expiration|null
* @return mixed|\Illuminate\Cache\CacheManager
*
* @throws \InvalidArgumentException
*/
function global_cache(): mixed
{ {
return app('globalCache'); $arguments = func_get_args();
if (empty($arguments)) {
return app('globalCache');
}
if (is_string($arguments[0])) {
return app('globalCache')->get(...$arguments);
}
if (! is_array($arguments[0])) {
throw new InvalidArgumentException(
'When setting a value in the cache, you must pass an array of key / value pairs.'
);
}
return app('globalCache')->put(key($arguments[0]), reset($arguments[0]), $arguments[1] ?? 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

@ -9,7 +9,6 @@ use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use PHPUnit\Framework\ExceptionWrapper;
use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Jobs\CreateDatabase;
use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseManager;
@ -217,8 +216,9 @@ test('run command with array of tenants works', function () {
Artisan::call('tenants:migrate-fresh'); Artisan::call('tenants:migrate-fresh');
pest()->artisan("tenants:run --tenants=$tenantId1 --tenants=$tenantId2 'foo foo --b=bar --c=xyz'") pest()->artisan("tenants:run --tenants=$tenantId1 --tenants=$tenantId2 'foo foo --b=bar --c=xyz'")
->expectsOutput('Tenant: ' . $tenantId1) ->expectsOutputToContain('Tenant: ' . $tenantId1)
->expectsOutput('Tenant: ' . $tenantId2); ->expectsOutputToContain('Tenant: ' . $tenantId2)
->assertExitCode(0);
}); });
test('link command works', function() { test('link command works', function() {

View file

@ -81,7 +81,7 @@ test('tenant can be identified by domain', function () {
test('onfail logic can be customized', function () { test('onfail logic can be customized', function () {
InitializeTenancyByDomain::$onFail = function () { InitializeTenancyByDomain::$onFail = function () {
return 'foo'; return response('foo');
}; };
pest() pest()

View file

@ -7,9 +7,13 @@ namespace Stancl\Tenancy\Tests\Etc;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains; use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
use Stancl\Tenancy\Database\Models; use Stancl\Tenancy\Database\Models;
/**
* @method static static create(array $attributes = [])
*/
class Tenant extends Models\Tenant implements TenantWithDatabase class Tenant extends Models\Tenant implements TenantWithDatabase
{ {
use HasDatabase, HasDomains; use HasDatabase, HasDomains, MaintenanceMode;
} }

View file

@ -50,3 +50,17 @@ test('global cache manager stores data in global cache', function () {
expect(cache('def'))->toBe('ghi'); expect(cache('def'))->toBe('ghi');
}); });
test('the global_cache helper supports the same syntax as the cache helper', function () {
$tenant = Tenant::create();
$tenant->enter();
expect(cache('foo'))->toBe(null); // tenant cache is empty
global_cache(['foo' => 'bar']);
expect(global_cache('foo'))->toBe('bar');
global_cache()->set('foo', 'baz');
expect(global_cache()->get('foo'))->toBe('baz');
expect(cache('foo'))->toBe(null); // tenant cache is not affected
});

View file

@ -2,14 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Support\Facades\Artisan;
use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
test('tenant can be in maintenance mode', function () { test('tenants can be in maintenance mode', function () {
Route::get('/foo', function () { Route::get('/foo', function () {
return 'bar'; return 'bar';
})->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]); })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
@ -19,16 +19,40 @@ test('tenant can be in maintenance mode', function () {
'domain' => 'acme.localhost', 'domain' => 'acme.localhost',
]); ]);
pest()->get('http://acme.localhost/foo') pest()->get('http://acme.localhost/foo')->assertStatus(200);
->assertSuccessful();
tenancy()->end(); // flush stored tenant instance
$tenant->putDownForMaintenance(); $tenant->putDownForMaintenance();
pest()->expectException(HttpException::class); tenancy()->end(); // End tenancy before making a request
pest()->withoutExceptionHandling() pest()->get('http://acme.localhost/foo')->assertStatus(503);
->get('http://acme.localhost/foo');
$tenant->bringUpFromMaintenance();
tenancy()->end(); // End tenancy before making a request
pest()->get('http://acme.localhost/foo')->assertStatus(200);
});
test('tenants can be put into maintenance mode using artisan commands', function() {
Route::get('/foo', function () {
return 'bar';
})->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
$tenant = MaintenanceTenant::create();
$tenant->domains()->create([
'domain' => 'acme.localhost',
]);
pest()->get('http://acme.localhost/foo')->assertStatus(200);
Artisan::call('tenants:down');
tenancy()->end(); // End tenancy before making a request
pest()->get('http://acme.localhost/foo')->assertStatus(503);
Artisan::call('tenants:up');
tenancy()->end(); // End tenancy before making a request
pest()->get('http://acme.localhost/foo')->assertStatus(200);
}); });
class MaintenanceTenant extends Tenant class MaintenanceTenant extends Tenant

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',
@ -71,7 +64,7 @@ test('exception is thrown when tenant cannot be identified by path', function ()
test('onfail logic can be customized', function () { test('onfail logic can be customized', function () {
InitializeTenancyByPath::$onFail = function () { InitializeTenancyByPath::$onFail = function () {
return 'foo'; return response('foo');
}; };
pest() pest()
@ -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

@ -37,7 +37,6 @@ test('header identification works', function () {
}); });
test('query parameter identification works', function () { test('query parameter identification works', function () {
InitializeTenancyByRequestData::$header = null;
InitializeTenancyByRequestData::$queryParameter = 'tenant'; InitializeTenancyByRequestData::$queryParameter = 'tenant';
$tenant = Tenant::create(); $tenant = Tenant::create();

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

@ -44,7 +44,7 @@ test('tenant can be identified by subdomain', function () {
test('onfail logic can be customized', function () { test('onfail logic can be customized', function () {
InitializeTenancyBySubdomain::$onFail = function () { InitializeTenancyBySubdomain::$onFail = function () {
return 'foo'; return response('foo');
}; };
pest() pest()

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,