1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 17:24:03 +00:00

Filesystem logic refactor, improved defaults for cache tenancy (#42)

* refactor FilesystemTenancyBootstrapper

* clean up tests and improve coverage

* minor maintenance mode changes

* Improve tenants:migrate --skip-failing logic

* make tenants:migrate output consistently formatted

* minor RootUrlBootstrapper + misc changes

* cache bootstrapper-related improvements

* Fix code style (php-cs-fixer)

* misc refactor

* Fix code style (php-cs-fixer)

* add %original_storage_path% to fs bootstrapper, improve default config for cache

* rename method

* inject concrete implementations where needed instead of abstracts

* Fix code style (php-cs-fixer)

* refactor DealsWithTenantSymlinks

* remove obsolete phpstan ignore

---------

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
This commit is contained in:
Samuel Štancl 2024-04-02 04:26:10 +02:00 committed by GitHub
parent 4b6fa22aa7
commit a41ad69023
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 234 additions and 160 deletions

View file

@ -20,6 +20,8 @@ use Illuminate\Support\Facades\Route as RouteFacade;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
// todo update all the docblock sections here, including the Livewire references
class TenancyServiceProvider extends ServiceProvider class TenancyServiceProvider extends ServiceProvider
{ {
// By default, no namespace is used to support the callable array syntax. // By default, no namespace is used to support the callable array syntax.
@ -66,6 +68,7 @@ class TenancyServiceProvider extends ServiceProvider
return $event->tenant; return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
], ],
Events\TenantMaintenanceModeEnabled::class => [], Events\TenantMaintenanceModeEnabled::class => [],
Events\TenantMaintenanceModeDisabled::class => [], Events\TenantMaintenanceModeDisabled::class => [],
@ -137,17 +140,21 @@ class TenancyServiceProvider extends ServiceProvider
protected function overrideUrlInTenantContext(): void protected function overrideUrlInTenantContext(): void
{ {
/** // Import your tenant model!
* Example of CLI tenant URL root override: // \Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant, string $originalRootUrl) {
* // $tenantDomain = $tenant instanceof \Stancl\Tenancy\Contracts\SingleDomainTenant
* RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant) { // ? $tenant->domain
* $baseUrl = env('APP_URL'); // : $tenant->domains->first()->domain;
* $scheme = str($baseUrl)->before('://'); //
* $hostname = str($baseUrl)->after($scheme . '://'); // $scheme = str($originalRootUrl)->before('://');
* //
* return $scheme . '://' . $tenant->getTenantKey() . '.' . $hostname; // // If you're using subdomain identification:
* }; // // $originalDomain = str($originalRootUrl)->after($scheme . '://');
*/ // // return $scheme . '://' . $tenantDomain . '.' . $originalDomain . '/';
//
// // If you're using domain identification:
// return $scheme . '://' . $tenantDomain . '/';
// };
} }
public function register() public function register()

View file

@ -205,19 +205,29 @@ return [
], ],
/** /**
* Cache tenancy config. Used by the custom CacheManager and the PrefixCacheTenancyBootstrapper. * Cache tenancy config. Used by the CacheTenancyBootstrapper, the CacheTagsBootstrapper, and the custom CacheManager.
* *
* This works for all Cache facade calls, cache() helper * This works for all Cache facade calls, cache() helper
* calls and direct calls to injected cache stores. * calls and direct calls to injected cache stores.
* *
* CacheTenancyBootstrapper:
* A prefix is applied *GLOBALLY*, using the `cache.prefix` config. This separates
* one tenant's cache from another's. The list of stores is used for refreshing
* them so that they re-load the prefix from the `cache.prefix` configuration.
*
* CacheTagsBootstrapper:
* Each key in cache will have a tag applied on it. This tag is used to * Each key in cache will have a tag applied on it. This tag is used to
* scope the cache both when writing to it and when reading from it. * scope the cache both when writing to it and when reading from it.
* *
* You can clear cache selectively by specifying the tag. * You can clear cache selectively by specifying the tag.
*/ */
'cache' => [ 'cache' => [
'prefix_base' => 'tenant', // This prefix_base, followed by the tenant_id, will form a cache prefix that will be used for every cache key.
'stores' => [
env('CACHE_STORE'),
],
'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call. 'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call.
'prefix_base' => 'tenant_', // This prefix_base, followed by the tenant_id, will form a cache prefix that will be used for every cache key.
], ],
/** /**
@ -297,6 +307,7 @@ return [
'prefix_base' => 'tenant', // Each key in Redis will be prepended by this prefix_base, followed by the tenant id. 'prefix_base' => 'tenant', // Each key in Redis will be prepended by this prefix_base, followed by the tenant id.
'prefixed_connections' => [ // Redis connections whose keys are prefixed, to separate one tenant's keys from another. 'prefixed_connections' => [ // Redis connections whose keys are prefixed, to separate one tenant's keys from another.
'default', 'default',
// 'cache', // Enable this if you want to scope cache using RedisTenancyBootstrapper
], ],
], ],

View file

@ -68,8 +68,8 @@
"phpstan": "vendor/bin/phpstan", "phpstan": "vendor/bin/phpstan",
"phpstan-pro": "vendor/bin/phpstan --pro", "phpstan-pro": "vendor/bin/phpstan --pro",
"cs": "php-cs-fixer fix --config=.php-cs-fixer.php", "cs": "php-cs-fixer fix --config=.php-cs-fixer.php",
"test": "./test --no-coverage --color=always", "test": "./test --no-coverage",
"test-full": "./test --color=always" "test-full": "./test"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,

View file

@ -40,10 +40,6 @@ parameters:
message: '#Illuminate\\Routing\\UrlGenerator#' message: '#Illuminate\\Routing\\UrlGenerator#'
paths: paths:
- src/Bootstrappers/FilesystemTenancyBootstrapper.php - src/Bootstrappers/FilesystemTenancyBootstrapper.php
-
message: '#Unable to resolve the template type (TMapWithKeysKey|TMapWithKeysValue) in call to method#'
paths:
- src/Concerns/DealsWithTenantSymlinks.php
- '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#' - '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#'
- '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#' - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#'
- '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#' - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#'

View file

@ -17,10 +17,10 @@ use Stancl\Tenancy\Contracts\Tenant;
*/ */
class CacheTenancyBootstrapper implements TenancyBootstrapper class CacheTenancyBootstrapper implements TenancyBootstrapper
{ {
protected string|null $originalPrefix = null;
public static array $tenantCacheStores = []; // E.g. ['redis']
public static Closure|null $prefixGenerator = null; public static Closure|null $prefixGenerator = null;
protected string|null $originalPrefix = null;
public function __construct( public function __construct(
protected ConfigRepository $config, protected ConfigRepository $config,
protected CacheManager $cacheManager, protected CacheManager $cacheManager,
@ -33,7 +33,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
$prefix = $this->generatePrefix($tenant); $prefix = $this->generatePrefix($tenant);
foreach (static::$tenantCacheStores as $store) { foreach ($this->config->get('tenancy.cache.stores') as $store) {
$this->setCachePrefix($store, $prefix); $this->setCachePrefix($store, $prefix);
// Now that the store uses the passed prefix // Now that the store uses the passed prefix
@ -44,7 +44,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
public function revert(): void public function revert(): void
{ {
foreach (static::$tenantCacheStores as $store) { foreach ($this->config->get('tenancy.cache.stores') as $store) {
$this->setCachePrefix($store, $this->originalPrefix); $this->setCachePrefix($store, $this->originalPrefix);
} }
} }

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers; namespace Stancl\Tenancy\Bootstrappers;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Routing\UrlGenerator; use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
@ -12,20 +12,15 @@ use Stancl\Tenancy\Contracts\Tenant;
class FilesystemTenancyBootstrapper implements TenancyBootstrapper class FilesystemTenancyBootstrapper implements TenancyBootstrapper
{ {
/** @var Application */ public array $originalDisks = [];
protected $app; public string|null $originalAssetUrl;
public string $originalStoragePath;
/** @var array */ public function __construct(
public $originalPaths = []; protected Application $app,
) {
public function __construct(Application $app) $this->originalAssetUrl = $this->app['config']['app.asset_url'];
{ $this->originalStoragePath = $app->storagePath();
$this->app = $app;
$this->originalPaths = [
'disks' => [],
'storage' => $this->app->storagePath(),
'asset_url' => $this->app['config']['app.asset_url'],
];
$this->app['url']->macro('setAssetRoot', function ($root) { $this->app['url']->macro('setAssetRoot', function ($root) {
/** @var UrlGenerator $this */ /** @var UrlGenerator $this */
@ -37,83 +32,128 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
public function bootstrap(Tenant $tenant): void public function bootstrap(Tenant $tenant): void
{ {
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->getTenantKey(); $suffix = $this->suffix($tenant);
// storage_path() $this->storagePath($suffix);
if ($this->app['config']['tenancy.filesystem.suffix_storage_path'] ?? true) { $this->assetHelper($suffix);
$this->app->useStoragePath($this->originalPaths['storage'] . "/{$suffix}"); $this->forgetDisks();
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
$this->diskRoot($disk, $tenant);
$this->diskUrl(
$disk,
str($this->app['config']["tenancy.filesystem.url_override.{$disk}"])
->replace('%tenant%', (string) $tenant->getTenantKey())
->toString(),
);
}
} }
// asset() public function revert(): void
if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy']) { {
if ($this->originalPaths['asset_url']) { $this->storagePath(false);
$this->app['config']['app.asset_url'] = $this->originalPaths['asset_url'] . "/$suffix"; $this->assetHelper(false);
$this->forgetDisks();
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
$this->diskRoot($disk, false);
$this->diskUrl($disk, false);
}
}
protected function suffix(Tenant $tenant): string
{
return $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->getTenantKey();
}
protected function storagePath(string|false $suffix): void
{
if ($this->app['config']['tenancy.filesystem.suffix_storage_path'] === false) {
return;
}
if ($suffix === false) {
$this->app->useStoragePath($this->originalStoragePath);
} else {
$this->app->useStoragePath($this->originalStoragePath . "/{$suffix}");
}
}
protected function assetHelper(string|false $suffix): void
{
if (! $this->app['config']['tenancy.filesystem.asset_helper_tenancy']) {
return;
}
if ($suffix === false) {
$this->app['config']['app.asset_url'] = $this->originalAssetUrl;
$this->app['url']->setAssetRoot($this->originalAssetUrl);
return;
}
if ($this->originalAssetUrl) {
$this->app['config']['app.asset_url'] = $this->originalAssetUrl . "/$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' => '']));
} }
} }
// Storage facade protected function forgetDisks(): void
{
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']); Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
}
protected function diskRoot(string $disk, Tenant|false $tenant): void
{
if ($tenant === false) {
$this->app['config']["filesystems.disks.$disk.root"] = $this->originalDisks[$disk]['root'];
return;
}
$suffix = $this->suffix($tenant);
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
// todo@v4 \League\Flysystem\PathPrefixer is making this a lot more painful in flysystem v2
$diskConfig = $this->app['config']["filesystems.disks.{$disk}"]; $diskConfig = $this->app['config']["filesystems.disks.{$disk}"];
$originalRoot = $diskConfig['root'] ?? null; $originalRoot = $diskConfig['root'] ?? null;
$this->originalPaths['disks']['path'][$disk] = $originalRoot; $this->originalDisks[$disk]['root'] = $originalRoot;
$finalPrefix = str_replace( if ($override = $this->app['config']["tenancy.filesystem.root_override.{$disk}"]) {
['%storage_path%', '%tenant%'], // This is executed if the disk is in tenancy.filesystem.disks AND has a root_override
[storage_path(), $tenant->getTenantKey()], // This behavior is used for local disks.
$this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '', $newRoot = str($override)
); ->replace('%storage_path%', storage_path())
->replace('%original_storage_path%', $this->originalStoragePath)
if (! $finalPrefix) { ->replace('%tenant%', (string) $tenant->getTenantKey())
$finalPrefix = $originalRoot ->toString();
} else {
// This is executed if the disk is in tenancy.filesystem.disks but does NOT have a root_override
// This behavior is used for disks like S3.
$newRoot = $originalRoot
? rtrim($originalRoot, '/') . '/' . $suffix ? rtrim($originalRoot, '/') . '/' . $suffix
: $suffix; : $suffix;
} }
$this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix; $this->app['config']["filesystems.disks.{$disk}.root"] = $newRoot;
// Storage Url
if ($diskConfig['driver'] === 'local') {
$this->originalPaths['disks']['url'][$disk] = $diskConfig['url'] ?? null;
if ($url = str_replace(
'%tenant%',
(string) $tenant->getTenantKey(),
$this->app['config']["tenancy.filesystem.url_override.{$disk}"] ?? ''
)) {
$this->app['config']["filesystems.disks.{$disk}.url"] = url($url);
}
}
}
} }
public function revert(): void protected function diskUrl(string $disk, string|false $override): void
{ {
// storage_path() $diskConfig = $this->app['config']["filesystems.disks.{$disk}"];
$this->app->useStoragePath($this->originalPaths['storage']);
// asset() if ($diskConfig['driver'] !== 'local' || $this->app['config']["tenancy.filesystem.url_override.{$disk}"] === null) {
$this->app['config']['app.asset_url'] = $this->originalPaths['asset_url']; return;
$this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
// Storage facade
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
foreach ($this->app['config']['tenancy.filesystem.disks'] as $diskName) {
$this->app['config']["filesystems.disks.$diskName.root"] = $this->originalPaths['disks']['path'][$diskName];
$diskConfig = $this->app['config']['filesystems.disks.' . $diskName];
// Storage Url
$url = data_get($this->originalPaths, "disks.url.$diskName");
if ($diskConfig['driver'] === 'local' && ! is_null($url)) {
$this->app['config']["filesystems.disks.$diskName.url"] = $url;
} }
if ($override === false) {
$url = data_get($this->originalDisks, "$disk.url");
$this->app['config']["filesystems.disks.$disk.url"] = $url;
} else {
$this->originalDisks[$disk]['url'] ??= $diskConfig['url'] ?? null;
$this->app['config']["filesystems.disks.{$disk}.url"] = url($override);
} }
} }
} }

View file

@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Bootstrappers;
use Closure; use Closure;
use Illuminate\Config\Repository; use Illuminate\Config\Repository;
use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Routing\UrlGenerator;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
@ -26,7 +26,7 @@ class RootUrlBootstrapper implements TenancyBootstrapper
$this->originalRootUrl = $this->urlGenerator->to('/'); $this->originalRootUrl = $this->urlGenerator->to('/');
if (static::$rootUrlOverride) { if (static::$rootUrlOverride) {
$newRootUrl = (static::$rootUrlOverride)($tenant); $newRootUrl = (static::$rootUrlOverride)($tenant, $this->originalRootUrl);
$this->urlGenerator->forceRootUrl($newRootUrl); $this->urlGenerator->forceRootUrl($newRootUrl);
$this->config->set('app.url', $newRootUrl); $this->config->set('app.url', $newRootUrl);

View file

@ -30,7 +30,7 @@ class Migrate extends MigrateCommand
{ {
parent::__construct($migrator, $dispatcher); parent::__construct($migrator, $dispatcher);
$this->addOption('skip-failing'); $this->addOption('skip-failing', description: 'Continue execution if migration fails for a tenant');
$this->specifyParameters(); $this->specifyParameters();
} }
@ -49,19 +49,21 @@ class Migrate extends MigrateCommand
foreach ($this->getTenants() as $tenant) { foreach ($this->getTenants() as $tenant) {
try { try {
$tenant->run(function ($tenant) { $this->components->info("Migrating tenant {$tenant->getTenantKey()}");
$this->line("Tenant: {$tenant->getTenantKey()}");
$tenant->run(function ($tenant) {
event(new MigratingDatabase($tenant)); event(new MigratingDatabase($tenant));
// Migrate // Migrate
parent::handle(); parent::handle();
event(new DatabaseMigrated($tenant)); event(new DatabaseMigrated($tenant));
}); });
} catch (TenantDatabaseDoesNotExistException|QueryException $th) { } catch (TenantDatabaseDoesNotExistException|QueryException $e) {
if (! $this->option('skip-failing')) { if (! $this->option('skip-failing')) {
throw $th; throw $e;
} }
$this->components->warn("Migration failed for tenant {$tenant->getTenantKey()}: {$e->getMessage()}");
} }
} }

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Concerns; namespace Stancl\Tenancy\Concerns;
use Illuminate\Support\Collection; use Exception;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
trait DealsWithTenantSymlinks trait DealsWithTenantSymlinks
@ -13,31 +13,44 @@ trait DealsWithTenantSymlinks
* Get all possible tenant symlinks, existing or not (array of ['public path' => 'storage path']). * Get all possible tenant symlinks, existing or not (array of ['public path' => 'storage path']).
* *
* 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.
* The same storage path can be symlinked to multiple public paths, which is why the public path
* is the Collection key.
* *
* @return Collection<string, string> * @return array<string, string>
*/ */
protected function possibleTenantSymlinks(Tenant $tenant): Collection protected function possibleTenantSymlinks(Tenant $tenant): array
{ {
$diskUrls = config('tenancy.filesystem.url_override'); $disks = config('filesystems.disks');
$disks = config('tenancy.filesystem.root_override'); $urlOverrides = config('tenancy.filesystem.url_override');
$suffixBase = config('tenancy.filesystem.suffix_base'); $rootOverrides = config('tenancy.filesystem.root_override');
$tenantKey = $tenant->getTenantKey(); $tenantKey = $tenant->getTenantKey();
$tenantStoragePath = tenancy()->run($tenant, fn () => storage_path());
/** @var Collection<array<string, string>> $symlinks */ /** @var array<string, string> $symlinks */
$symlinks = collect([]); $symlinks = [];
foreach ($diskUrls as $disk => $publicPath) { foreach ($urlOverrides as $disk => $publicPath) {
$storagePath = str_replace('%storage_path%', $suffixBase . $tenantKey, $disks[$disk]); if (! isset($disks[$disk])) {
$publicPath = str_replace('%tenant%', (string) $tenantKey, $publicPath); continue;
tenancy()->central(function () use ($symlinks, $publicPath, $storagePath) {
$symlinks->push([public_path($publicPath) => storage_path($storagePath)]);
});
} }
return $symlinks->mapWithKeys(fn ($item) => $item); // [[a => b], [c => d]] -> [a => b, c => d] if (! isset($rootOverrides[$disk])) {
continue;
}
if ($disks[$disk]['driver'] !== 'local') {
throw new Exception("Disk $disk is not a local disk. Only local disks can be symlinked.");
}
$publicPath = str_replace('%tenant%', (string) $tenantKey, $publicPath);
$storagePath = str_replace('%storage_path%', $tenantStoragePath, $rootOverrides[$disk]);
$symlinks[public_path($publicPath)] = $storagePath;
}
return $symlinks;
} }
/** Determine if the provided path is an existing symlink. */ /** Determine if the provided path is an existing symlink. */

View file

@ -16,8 +16,8 @@ trait HasTenantOptions
protected function getOptions() protected function getOptions()
{ {
return array_merge([ return array_merge([
['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, '', null], ['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null],
['with-pending', null, InputOption::VALUE_NONE, 'include pending tenants in query'], ['with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'],
], parent::getOptions()); ], parent::getOptions());
} }

View file

@ -27,7 +27,7 @@ class InitializeTenancyByDomainOrSubdomain extends InitializeTenancyBySubdomain
if ($this->isSubdomain($domain)) { if ($this->isSubdomain($domain)) {
$domain = $this->makeSubdomain($domain); $domain = $this->makeSubdomain($domain);
if (is_object($domain) && $domain instanceof Exception) { if ($domain instanceof Exception) {
$onFail = static::$onFail ?? function ($e) { $onFail = static::$onFail ?? function ($e) {
throw $e; throw $e;
}; };
@ -36,7 +36,7 @@ class InitializeTenancyByDomainOrSubdomain extends InitializeTenancyBySubdomain
} }
// If a Response instance was returned, we return it immediately. // If a Response instance was returned, we return it immediately.
if (is_object($domain) && $domain instanceof Response) { if ($domain instanceof Response) {
return $domain; return $domain;
} }
} }

View file

@ -37,7 +37,7 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
$subdomain = $this->makeSubdomain($this->getDomain($request)); $subdomain = $this->makeSubdomain($this->getDomain($request));
if (is_object($subdomain) && $subdomain instanceof Exception) { if ($subdomain instanceof Exception) {
$onFail = static::$onFail ?? function ($e) { $onFail = static::$onFail ?? function ($e) {
throw $e; throw $e;
}; };
@ -46,7 +46,7 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
} }
// If a Response instance was returned, we return it immediately. // If a Response instance was returned, we return it immediately.
if (is_object($subdomain) && $subdomain instanceof Response) { if ($subdomain instanceof Response) {
return $subdomain; return $subdomain;
} }

View file

@ -31,7 +31,7 @@ class Tenancy
/** Initialize tenancy for the passed tenant. */ /** Initialize tenancy for the passed tenant. */
public function initialize(Tenant|int|string $tenant): void public function initialize(Tenant|int|string $tenant): void
{ {
if (! is_object($tenant)) { if (! $tenant instanceof Tenant) {
$tenantId = $tenant; $tenantId = $tenant;
$tenant = $this->find($tenantId); $tenant = $this->find($tenantId);

View file

@ -130,7 +130,7 @@ class TenancyServiceProvider extends ServiceProvider
$this->app->singleton('globalUrl', function ($app) { $this->app->singleton('globalUrl', function ($app) {
if ($app->bound(FilesystemTenancyBootstrapper::class)) { if ($app->bound(FilesystemTenancyBootstrapper::class)) {
$instance = clone $app['url']; $instance = clone $app['url'];
$instance->setAssetRoot($app[FilesystemTenancyBootstrapper::class]->originalPaths['asset_url']); $instance->setAssetRoot($app[FilesystemTenancyBootstrapper::class]->originalAssetUrl);
} else { } else {
$instance = $app['url']; $instance = $app['url'];
} }

2
test
View file

@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
# --columns doesn't seem to work at the moment, so we're setting it using an environment variable # --columns doesn't seem to work at the moment, so we're setting it using an environment variable
docker-compose exec -e COLUMNS=$(tput cols) -T test vendor/bin/pest "$@" docker-compose exec -e COLUMNS=$(tput cols) -T test vendor/bin/pest --colors=always "$@"

View file

@ -50,11 +50,15 @@ use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
use Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteBootstrapper; use Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteBootstrapper;
// todo refactor this file -- too much stuff happening here
beforeEach(function () { beforeEach(function () {
$this->mockConsoleOutput = false; $this->mockConsoleOutput = false;
config(['cache.default' => $cacheDriver = 'redis']); config([
CacheTenancyBootstrapper::$tenantCacheStores = [$cacheDriver]; 'cache.default' => 'redis',
'tenancy.cache.stores' => ['redis'],
]);
// Reset static properties of classes used in this test file to their default values // Reset static properties of classes used in this test file to their default values
BroadcastingConfigBootstrapper::$credentialsMap = []; BroadcastingConfigBootstrapper::$credentialsMap = [];
TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably']; TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably'];
@ -74,7 +78,6 @@ beforeEach(function () {
afterEach(function () { afterEach(function () {
// Reset static properties of classes used in this test file to their default values // Reset static properties of classes used in this test file to their default values
RootUrlBootstrapper::$rootUrlOverride = null; RootUrlBootstrapper::$rootUrlOverride = null;
CacheTenancyBootstrapper::$tenantCacheStores = [];
TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably']; TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably'];
BroadcastingConfigBootstrapper::$credentialsMap = []; BroadcastingConfigBootstrapper::$credentialsMap = [];
TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$prefixRouteNames = false;

View file

@ -19,9 +19,9 @@ beforeEach(function () {
], ],
'cache.default' => $cacheDriver = 'redis', 'cache.default' => $cacheDriver = 'redis',
'cache.stores.' . $secondCacheDriver = 'redis2' => config('cache.stores.redis'), 'cache.stores.' . $secondCacheDriver = 'redis2' => config('cache.stores.redis'),
'tenancy.cache.stores' => [$cacheDriver, $secondCacheDriver],
]); ]);
CacheTenancyBootstrapper::$tenantCacheStores = [$cacheDriver, $secondCacheDriver];
CacheTenancyBootstrapper::$prefixGenerator = null; CacheTenancyBootstrapper::$prefixGenerator = null;
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
@ -29,7 +29,6 @@ beforeEach(function () {
}); });
afterEach(function () { afterEach(function () {
CacheTenancyBootstrapper::$tenantCacheStores = [];
CacheTenancyBootstrapper::$prefixGenerator = null; CacheTenancyBootstrapper::$prefixGenerator = null;
}); });
@ -180,7 +179,7 @@ test('cache is prefixed correctly when using a repository injected in a singleto
test('specific central cache store can be used inside a service', function () { test('specific central cache store can be used inside a service', function () {
// Make sure 'redis' (the default store) is the only prefixed store // Make sure 'redis' (the default store) is the only prefixed store
CacheTenancyBootstrapper::$tenantCacheStores = ['redis']; config(['tenancy.cache.stores' => ['redis']]);
// Name of the non-default, central cache store that we'll use using cache()->store($cacheStore) // Name of the non-default, central cache store that we'll use using cache()->store($cacheStore)
$cacheStore = 'redis2'; $cacheStore = 'redis2';
@ -217,7 +216,7 @@ test('specific central cache store can be used inside a service', function () {
test('only the stores specified in tenantCacheStores get prefixed', function () { test('only the stores specified in tenantCacheStores get prefixed', function () {
// Make sure the currently used store ('redis') is the only store in $tenantCacheStores // Make sure the currently used store ('redis') is the only store in $tenantCacheStores
CacheTenancyBootstrapper::$tenantCacheStores = [$prefixedStore = 'redis']; config(['tenancy.cache.stores' => [$prefixedStore = 'redis']]);
$centralValue = 'central-value'; $centralValue = 'central-value';
$assertStoreIsNotPrefixed = function (string $unprefixedStore) use ($prefixedStore, $centralValue) { $assertStoreIsNotPrefixed = function (string $unprefixedStore) use ($prefixedStore, $centralValue) {

View file

@ -24,6 +24,7 @@ use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException; use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
beforeEach(function () { beforeEach(function () {
@ -298,6 +299,8 @@ test('run command with array of tenants works', function () {
}); });
test('link command works', function() { test('link command works', function() {
config(['tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class]]);
$tenantId1 = Tenant::create()->getTenantKey(); $tenantId1 = Tenant::create()->getTenantKey();
$tenantId2 = Tenant::create()->getTenantKey(); $tenantId2 = Tenant::create()->getTenantKey();
pest()->artisan('tenants:link') pest()->artisan('tenants:link')
@ -318,6 +321,8 @@ test('link command works', function() {
}); });
test('link command works with a specified tenant', function() { test('link command works with a specified tenant', function() {
config(['tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class]]);
$tenantKey = Tenant::create()->getTenantKey(); $tenantKey = Tenant::create()->getTenantKey();
pest()->artisan('tenants:link', [ pest()->artisan('tenants:link', [

View file

@ -8,7 +8,6 @@ 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\HasPending; use Stancl\Tenancy\Database\Concerns\HasPending;
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
use Stancl\Tenancy\Database\Models; use Stancl\Tenancy\Database\Models;
/** /**
@ -18,7 +17,7 @@ class Tenant extends Models\Tenant implements TenantWithDatabase
{ {
public static array $extraCustomColumns = []; public static array $extraCustomColumns = [];
use HasDatabase, HasDomains, HasPending, MaintenanceMode; use HasDatabase, HasDomains, HasPending;
public static function getCustomColumns(): array public static function getCustomColumns(): array
{ {

View file

@ -2,7 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
use Stancl\Tenancy\Overrides\CacheManager;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyEnded;
@ -14,17 +13,15 @@ use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
beforeEach(function () { beforeEach(function () {
config(['cache.default' => $cacheDriver = 'redis']); config([
CacheTenancyBootstrapper::$tenantCacheStores = [$cacheDriver]; 'cache.default' => 'redis',
'tenancy.cache.stores' => ['redis'],
]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class);
}); });
afterEach(function () {
CacheTenancyBootstrapper::$tenantCacheStores = [];
});
test('global cache manager stores data in global cache', function (string $bootstrapper) { test('global cache manager stores data in global cache', function (string $bootstrapper) {
config(['tenancy.bootstrappers' => [$bootstrapper]]); config(['tenancy.bootstrappers' => [$bootstrapper]]);

View file

@ -10,6 +10,10 @@ 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;
beforeEach(function () {
config(['tenancy.models.tenant' => MaintenanceTenant::class]);
});
test('tenants 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';

View file

@ -16,6 +16,8 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper; use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
use Stancl\Tenancy\Controllers\TenantAssetController; use Stancl\Tenancy\Controllers\TenantAssetController;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Overrides\TenancyUrlGenerator; use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
beforeEach(function () { beforeEach(function () {
@ -32,6 +34,7 @@ beforeEach(function () {
$cloneAction->handle(Route::getRoutes()->getByName('stancl.tenancy.asset')); $cloneAction->handle(Route::getRoutes()->getByName('stancl.tenancy.asset'));
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::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 () {
@ -68,6 +71,9 @@ test('asset helper returns a link to tenant asset controller when asset url is n
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo'])); expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo']));
tenancy()->end();
expect(asset('foo'))->toBe('http://localhost/foo');
}); });
test('asset helper returns a link to an external url when asset url is not null', function () { test('asset helper returns a link to an external url when asset url is not null', function () {
@ -78,6 +84,9 @@ test('asset helper returns a link to an external url when asset url is not null'
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
expect(asset('foo'))->toBe("https://an-s3-bucket/tenant{$tenant->id}/foo"); expect(asset('foo'))->toBe("https://an-s3-bucket/tenant{$tenant->id}/foo");
tenancy()->end();
expect(asset('foo'))->toBe('https://an-s3-bucket/foo');
}); });
test('asset helper works correctly with path identification', function (bool $kernelIdentification) { test('asset helper works correctly with path identification', function (bool $kernelIdentification) {
@ -243,14 +252,3 @@ test('test asset controller returns a 404 when accessing a file outside the stor
'X-Tenant' => $tenant->id, 'X-Tenant' => $tenant->id,
]); ]);
}); });
function getEnvironmentSetUp($app)
{
$app->booted(function () {
if (file_exists(base_path('routes/tenant.php'))) {
Route::middleware(['web'])
->namespace(pest()->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers')
->group(base_path('routes/tenant.php'));
}
});
}