mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 19:14:03 +00:00
Merge branch 'master' into 515-complete
This commit is contained in:
commit
69e4a1ef2a
102 changed files with 1539 additions and 558 deletions
|
|
@ -10,6 +10,7 @@ $rules = [
|
|||
'operators' => [
|
||||
'=>' => null,
|
||||
'|' => 'no_space',
|
||||
'&' => 'no_space',
|
||||
]
|
||||
],
|
||||
'blank_line_after_namespace' => true,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
Jobs\CreateDatabase::class,
|
||||
Jobs\MigrateDatabase::class,
|
||||
// Jobs\SeedDatabase::class,
|
||||
Jobs\CreateStorageSymlinks::class,
|
||||
|
||||
// Your own jobs to prepare the tenant.
|
||||
// Provision API keys, create S3 buckets, anything you want!
|
||||
|
|
@ -46,10 +47,13 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
])->send(function (Events\DeletingTenant $event) {
|
||||
return $event->tenant;
|
||||
})->shouldBeQueued(false),
|
||||
|
||||
// Listeners\DeleteTenantStorage::class,
|
||||
],
|
||||
Events\TenantDeleted::class => [
|
||||
JobPipeline::make([
|
||||
Jobs\DeleteDatabase::class,
|
||||
Jobs\RemoveStorageSymlinks::class,
|
||||
])->send(function (Events\TenantDeleted $event) {
|
||||
return $event->tenant;
|
||||
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
||||
|
|
@ -93,6 +97,12 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
Listeners\UpdateSyncedResource::class,
|
||||
],
|
||||
|
||||
// Storage symlinks
|
||||
Events\CreatingStorageSymlink::class => [],
|
||||
Events\StorageSymlinkCreated::class => [],
|
||||
Events\RemovingStorageSymlink::class => [],
|
||||
Events\StorageSymlinkRemoved::class => [],
|
||||
|
||||
// Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
|
||||
Events\SyncedResourceChangedInForeignDatabase::class => [],
|
||||
];
|
||||
|
|
@ -134,16 +144,8 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
|
||||
protected function makeTenancyMiddlewareHighestPriority()
|
||||
{
|
||||
$tenancyMiddleware = [
|
||||
// Even higher priority than the initialization middleware
|
||||
Middleware\PreventAccessFromCentralDomains::class,
|
||||
|
||||
Middleware\InitializeTenancyByDomain::class,
|
||||
Middleware\InitializeTenancyBySubdomain::class,
|
||||
Middleware\InitializeTenancyByDomainOrSubdomain::class,
|
||||
Middleware\InitializeTenancyByPath::class,
|
||||
Middleware\InitializeTenancyByRequestData::class,
|
||||
];
|
||||
// PreventAccessFromCentralDomains has even higher priority than the identification middleware
|
||||
$tenancyMiddleware = array_merge([Middleware\PreventAccessFromCentralDomains::class], config('tenancy.identification.middleware'));
|
||||
|
||||
foreach (array_reverse($tenancyMiddleware) as $middleware) {
|
||||
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
use Stancl\Tenancy\Database\Models\Domain;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Middleware;
|
||||
use Stancl\Tenancy\Resolvers;
|
||||
|
||||
return [
|
||||
'tenant_model' => Tenant::class,
|
||||
|
|
@ -21,6 +23,56 @@ return [
|
|||
'localhost',
|
||||
],
|
||||
|
||||
'identification' => [
|
||||
/**
|
||||
* The default middleware used for tenant identification.
|
||||
*
|
||||
* If you use multiple forms of identification, you can set this to the "main" approach you use.
|
||||
*/
|
||||
'default_middleware' => Middleware\InitializeTenancyByDomain::class,// todo@identification add this to a 'tenancy' mw group
|
||||
|
||||
/**
|
||||
* All of the identification middleware used by the package.
|
||||
*
|
||||
* If you write your own, make sure to add them to this array.
|
||||
*/
|
||||
'middleware' => [
|
||||
Middleware\InitializeTenancyByDomain::class,
|
||||
Middleware\InitializeTenancyBySubdomain::class,
|
||||
Middleware\InitializeTenancyByDomainOrSubdomain::class,
|
||||
Middleware\InitializeTenancyByPath::class,
|
||||
Middleware\InitializeTenancyByRequestData::class,
|
||||
],
|
||||
|
||||
/**
|
||||
* Tenant resolvers used by the package.
|
||||
*
|
||||
* Resolvers which implement the CachedTenantResolver contract have options for configuring the caching details.
|
||||
* If you add your own resolvers, do not add the 'cache' key unless your resolver is based on CachedTenantResolver.
|
||||
*/
|
||||
'resolvers' => [
|
||||
Resolvers\DomainTenantResolver::class => [
|
||||
'cache' => false,
|
||||
'cache_ttl' => 3600, // seconds
|
||||
'cache_store' => null, // default
|
||||
],
|
||||
Resolvers\PathTenantResolver::class => [
|
||||
'tenant_parameter_name' => 'tenant',
|
||||
|
||||
'cache' => false,
|
||||
'cache_ttl' => 3600, // seconds
|
||||
'cache_store' => null, // default
|
||||
],
|
||||
Resolvers\RequestDataTenantResolver::class => [
|
||||
'cache' => false,
|
||||
'cache_ttl' => 3600, // seconds
|
||||
'cache_store' => null, // default
|
||||
],
|
||||
],
|
||||
|
||||
// todo@docs update integration guides to use Stancl\Tenancy::defaultMiddleware()
|
||||
],
|
||||
|
||||
/**
|
||||
* Tenancy bootstrappers are executed when tenancy is initialized.
|
||||
* Their responsibility is making Laravel features tenant-aware.
|
||||
|
|
@ -32,6 +84,7 @@ return [
|
|||
Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
|
||||
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
|
||||
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
|
||||
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
|
||||
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
||||
],
|
||||
|
||||
|
|
@ -123,6 +176,24 @@ return [
|
|||
'public' => '%storage_path%/app/public/',
|
||||
],
|
||||
|
||||
/*
|
||||
* Tenant-aware Storage::disk()->url() can be enabled for specific local disks here
|
||||
* by mapping the disk's name to a name with '%tenant_id%' (this will be used as the public name of the disk).
|
||||
* Doing that will override the disk's default URL with a URL containing the current tenant's key.
|
||||
*
|
||||
* For example, Storage::disk('public')->url('') will return https://your-app.test/storage/ by default.
|
||||
* After adding 'public' => 'public-%tenant_id%' to 'url_override',
|
||||
* the returned URL will be https://your-app.test/public-1/ (%tenant_id% gets substitued by the current tenant's ID).
|
||||
*
|
||||
* Use `php artisan tenants:link` to create a symbolic link from the tenant's storage to its public directory.
|
||||
*/
|
||||
'url_override' => [
|
||||
// Note that the local disk you add must exist in the tenancy.filesystem.root_override config
|
||||
// todo@v4 Rename %tenant_id% to %tenant_key%
|
||||
// todo@v4 Rename url_override to something that describes the config key better
|
||||
'public' => 'public-%tenant_id%',
|
||||
],
|
||||
|
||||
/**
|
||||
* Should storage_path() be suffixed.
|
||||
*
|
||||
|
|
@ -201,4 +272,12 @@ return [
|
|||
'--class' => 'DatabaseSeeder', // root seeder class
|
||||
// '--force' => true,
|
||||
],
|
||||
|
||||
/**
|
||||
* Single-database tenancy config.
|
||||
*/
|
||||
'single_db' => [
|
||||
/** The name of the column used by models with the BelongsToTenant trait. */
|
||||
'tenant_id_column' => 'tenant_id',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Controllers\TenantAssetController;
|
||||
|
||||
Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset')
|
||||
Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset'])
|
||||
->where('path', '(.*)')
|
||||
->name('stancl.tenancy.asset');
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
|
||||
"coverage": "open coverage/phpunit/html/index.html",
|
||||
"phpstan": "vendor/bin/phpstan",
|
||||
"cs": "php-cs-fixer fix --config=.php-cs-fixer.php",
|
||||
"test": "PHP_VERSION=8.1 ./test --no-coverage",
|
||||
"test-full": "PHP_VERSION=8.1 ./test"
|
||||
},
|
||||
|
|
|
|||
31
phpstan.neon
31
phpstan.neon
|
|
@ -10,16 +10,39 @@ parameters:
|
|||
|
||||
universalObjectCratesClasses:
|
||||
- Illuminate\Routing\Route
|
||||
- Illuminate\Database\Eloquent\Model
|
||||
|
||||
ignoreErrors:
|
||||
-
|
||||
message: '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#'
|
||||
paths:
|
||||
- src/TenancyServiceProvider.php
|
||||
- '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#'
|
||||
- '#Cannot access offset (.*?) on Illuminate\\Contracts\\Config\\Repository#'
|
||||
-
|
||||
message: '#invalid type Laravel\\Telescope\\IncomingEntry#'
|
||||
paths:
|
||||
- src/Features/TelescopeTags.php
|
||||
-
|
||||
message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getRelationshipToPrimaryModel\(\)#'
|
||||
paths:
|
||||
- src/Database/ParentModelScope.php
|
||||
-
|
||||
message: '#Parameter \#1 \$key of method Illuminate\\Contracts\\Cache\\Repository::put\(\) expects string#'
|
||||
paths:
|
||||
- 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
|
||||
treatPhpDocTypesAsCertain: false
|
||||
|
|
|
|||
|
|
@ -35,4 +35,4 @@
|
|||
<log type="coverage-clover" target="coverage/phpunit/clover.xml" showUncoveredFiles="true"/>
|
||||
<log type="coverage-html" target="coverage/phpunit/html" lowUpperBound="35" highLowerBound="70"/>
|
||||
</logging>
|
||||
</phpunit>
|
||||
</phpunit>
|
||||
|
|
|
|||
55
src/Actions/CreateStorageSymlinksAction.php
Normal file
55
src/Actions/CreateStorageSymlinksAction.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Actions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Events\CreatingStorageSymlink;
|
||||
use Stancl\Tenancy\Events\StorageSymlinkCreated;
|
||||
|
||||
class CreateStorageSymlinksAction
|
||||
{
|
||||
use DealsWithTenantSymlinks;
|
||||
|
||||
public static function handle(Tenant|Collection|LazyCollection $tenants, bool $relativeLink = false, bool $force = false): void
|
||||
{
|
||||
$tenants = $tenants instanceof Tenant ? collect([$tenants]) : $tenants;
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
foreach ($tenants as $tenant) {
|
||||
foreach (static::possibleTenantSymlinks($tenant) as $publicPath => $storagePath) {
|
||||
static::createLink($publicPath, $storagePath, $tenant, $relativeLink, $force);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static function createLink(string $publicPath, string $storagePath, Tenant $tenant, bool $relativeLink, bool $force): void
|
||||
{
|
||||
event(new CreatingStorageSymlink($tenant));
|
||||
|
||||
if (static::symlinkExists($publicPath)) {
|
||||
// If $force isn't passed, don't overwrite the existing symlink
|
||||
throw_if(! $force, new Exception("The [$publicPath] link already exists."));
|
||||
|
||||
app()->make('files')->delete($publicPath);
|
||||
}
|
||||
|
||||
// Make sure the storage path exists before we create a symlink
|
||||
if (! is_dir($storagePath)) {
|
||||
mkdir($storagePath, 0777, true);
|
||||
}
|
||||
|
||||
if ($relativeLink) {
|
||||
app()->make('files')->relativeLink($storagePath, $publicPath);
|
||||
} else {
|
||||
app()->make('files')->link($storagePath, $publicPath);
|
||||
}
|
||||
|
||||
event((new StorageSymlinkCreated($tenant)));
|
||||
}
|
||||
}
|
||||
40
src/Actions/RemoveStorageSymlinksAction.php
Normal file
40
src/Actions/RemoveStorageSymlinksAction.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Actions;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Events\RemovingStorageSymlink;
|
||||
use Stancl\Tenancy\Events\StorageSymlinkRemoved;
|
||||
|
||||
class RemoveStorageSymlinksAction
|
||||
{
|
||||
use DealsWithTenantSymlinks;
|
||||
|
||||
public static function handle(Tenant|Collection|LazyCollection $tenants): void
|
||||
{
|
||||
$tenants = $tenants instanceof Tenant ? collect([$tenants]) : $tenants;
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
foreach ($tenants as $tenant) {
|
||||
foreach (static::possibleTenantSymlinks($tenant) as $publicPath => $storagePath) {
|
||||
static::removeLink($publicPath, $tenant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static function removeLink(string $publicPath, Tenant $tenant): void
|
||||
{
|
||||
if (static::symlinkExists($publicPath)) {
|
||||
event(new RemovingStorageSymlink($tenant));
|
||||
|
||||
app()->make('files')->delete($publicPath);
|
||||
|
||||
event(new StorageSymlinkRemoved($tenant));
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Bootstrappers/BatchTenancyBootstrapper.php
Normal file
41
src/Bootstrappers/BatchTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Bootstrappers;
|
||||
|
||||
use Illuminate\Bus\DatabaseBatchRepository;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
class BatchTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/**
|
||||
* The previous database connection instance.
|
||||
*/
|
||||
protected ?Connection $previousConnection = null;
|
||||
|
||||
public function __construct(
|
||||
protected DatabaseBatchRepository $batchRepository,
|
||||
protected DatabaseManager $databaseManager
|
||||
) {
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
// Update batch repository connection to use the tenant connection
|
||||
$this->previousConnection = $this->batchRepository->getConnection();
|
||||
$this->batchRepository->setConnection($this->databaseManager->connection('tenant'));
|
||||
}
|
||||
|
||||
public function revert(): void
|
||||
{
|
||||
if ($this->previousConnection) {
|
||||
// Replace batch repository connection with the previously replaced one
|
||||
$this->batchRepository->setConnection($this->previousConnection);
|
||||
$this->previousConnection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,18 +13,14 @@ use Stancl\Tenancy\Contracts\Tenant;
|
|||
|
||||
class CacheTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/** @var CacheManager */
|
||||
protected $originalCache;
|
||||
protected ?CacheManager $originalCache = null;
|
||||
|
||||
/** @var Application */
|
||||
protected $app;
|
||||
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
public function __construct(
|
||||
protected Application $app
|
||||
) {
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant)
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
$this->resetFacadeCache();
|
||||
|
||||
|
|
@ -34,7 +30,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
|
|||
});
|
||||
}
|
||||
|
||||
public function revert()
|
||||
public function revert(): void
|
||||
{
|
||||
$this->resetFacadeCache();
|
||||
|
||||
|
|
@ -50,7 +46,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
|
|||
* facade has been made prior to bootstrapping tenancy. The
|
||||
* facade has its own cache, separate from the container.
|
||||
*/
|
||||
public function resetFacadeCache()
|
||||
public function resetFacadeCache(): void
|
||||
{
|
||||
Cache::clearResolvedInstances();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
|||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException;
|
||||
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||
|
||||
class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
|
|
@ -20,7 +20,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
|||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant)
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
/** @var TenantWithDatabase $tenant */
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
|||
$this->database->connectToTenant($tenant);
|
||||
}
|
||||
|
||||
public function revert()
|
||||
public function revert(): void
|
||||
{
|
||||
$this->database->reconnectToCentral();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Bootstrappers;
|
||||
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Routing\UrlGenerator;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
|
@ -27,13 +28,14 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
];
|
||||
|
||||
$this->app['url']->macro('setAssetRoot', function ($root) {
|
||||
/** @var UrlGenerator $this */
|
||||
$this->assetRoot = $root;
|
||||
|
||||
return $this;
|
||||
});
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant)
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->getTenantKey();
|
||||
|
||||
|
|
@ -45,7 +47,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
// asset()
|
||||
if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy'] ?? true) {
|
||||
if ($this->originalPaths['asset_url']) {
|
||||
$this->app['config']['app.asset_url'] = ($this->originalPaths['asset_url'] ?? $this->app['config']['app.url']) . "/$suffix";
|
||||
$this->app['config']['app.asset_url'] = $this->originalPaths['asset_url'] . "/$suffix";
|
||||
$this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
|
||||
} else {
|
||||
$this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => '']));
|
||||
|
|
@ -57,9 +59,10 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
|
||||
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}"];
|
||||
$originalRoot = $diskConfig['root'] ?? null;
|
||||
|
||||
$originalRoot = $this->app['config']["filesystems.disks.{$disk}.root"];
|
||||
$this->originalPaths['disks'][$disk] = $originalRoot;
|
||||
$this->originalPaths['disks']['path'][$disk] = $originalRoot;
|
||||
|
||||
$finalPrefix = str_replace(
|
||||
['%storage_path%', '%tenant%'],
|
||||
|
|
@ -74,10 +77,23 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
}
|
||||
|
||||
$this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix;
|
||||
|
||||
// Storage Url
|
||||
if ($diskConfig['driver'] === 'local') {
|
||||
$this->originalPaths['disks']['url'][$disk] = $diskConfig['url'] ?? null;
|
||||
|
||||
if ($url = str_replace(
|
||||
'%tenant_id%',
|
||||
(string) $tenant->getTenantKey(),
|
||||
$this->app['config']["tenancy.filesystem.url_override.{$disk}"] ?? ''
|
||||
)) {
|
||||
$this->app['config']["filesystems.disks.{$disk}.url"] = url($url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function revert()
|
||||
public function revert(): void
|
||||
{
|
||||
// storage_path()
|
||||
$this->app->useStoragePath($this->originalPaths['storage']);
|
||||
|
|
@ -88,8 +104,16 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
|
||||
// Storage facade
|
||||
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
|
||||
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
||||
$this->app['config']["filesystems.disks.{$disk}.root"] = $this->originalPaths['disks'][$disk];
|
||||
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 = $this->originalPaths['disks.url.' . $diskName] ?? null;
|
||||
|
||||
if ($diskConfig['driver'] === 'local' && ! is_null($url)) {
|
||||
$$this->app['config']["filesystems.disks.$diskName.url"] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,27 +10,23 @@ use Stancl\Tenancy\Contracts\Tenant;
|
|||
|
||||
class ScoutTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/** @var Repository */
|
||||
protected $config;
|
||||
protected ?string $originalScoutPrefix = null;
|
||||
|
||||
/** @var string */
|
||||
protected $originalScoutPrefix;
|
||||
|
||||
public function __construct(Repository $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
public function __construct(
|
||||
protected Repository $config,
|
||||
) {
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant)
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
if (! isset($this->originalScoutPrefix)) {
|
||||
if ($this->originalScoutPrefix !== null) {
|
||||
$this->originalScoutPrefix = $this->config->get('scout.prefix');
|
||||
}
|
||||
|
||||
$this->config->set('scout.prefix', $this->getTenantPrefix($tenant));
|
||||
}
|
||||
|
||||
public function revert()
|
||||
public function revert(): void
|
||||
{
|
||||
$this->config->set('scout.prefix', $this->originalScoutPrefix);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
* However, we're registering a hook to initialize tenancy. Therefore,
|
||||
* we need to register the hook at service provider execution time.
|
||||
*/
|
||||
public static function __constructStatic(Application $app)
|
||||
public static function __constructStatic(Application $app): void
|
||||
{
|
||||
static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests());
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
$this->setUpPayloadGenerator();
|
||||
}
|
||||
|
||||
protected static function setUpJobListener($dispatcher, $runningTests)
|
||||
protected static function setUpJobListener(Dispatcher $dispatcher, bool $runningTests): void
|
||||
{
|
||||
$previousTenant = null;
|
||||
|
||||
|
|
@ -62,14 +62,11 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
|
||||
if (version_compare(app()->version(), '8.64', '>=')) {
|
||||
// JobRetryRequested only exists since Laravel 8.64
|
||||
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
|
||||
$previousTenant = tenant();
|
||||
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
|
||||
$previousTenant = tenant();
|
||||
|
||||
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
}
|
||||
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
|
||||
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
|
||||
$revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) {
|
||||
|
|
@ -82,7 +79,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
|
||||
}
|
||||
|
||||
protected static function initializeTenancyForQueue($tenantId)
|
||||
protected static function initializeTenancyForQueue(string|int $tenantId): void
|
||||
{
|
||||
if (! $tenantId) {
|
||||
// The job is not tenant-aware
|
||||
|
|
@ -100,7 +97,9 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
tenancy()->end();
|
||||
}
|
||||
|
||||
tenancy()->initialize(tenancy()->find($tenantId));
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = tenancy()->find($tenantId);
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -115,10 +114,13 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
|
||||
// Tenancy was either not initialized, or initialized for a different tenant.
|
||||
// Therefore, we initialize it for the correct tenant.
|
||||
tenancy()->initialize(tenancy()->find($tenantId));
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = tenancy()->find($tenantId);
|
||||
tenancy()->initialize($tenant);
|
||||
}
|
||||
|
||||
protected static function revertToPreviousState($event, &$previousTenant)
|
||||
protected static function revertToPreviousState(JobProcessed|JobFailed $event, ?Tenant &$previousTenant): void
|
||||
{
|
||||
$tenantId = $event->job->payload()['tenant_id'] ?? null;
|
||||
|
||||
|
|
@ -138,7 +140,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
}
|
||||
}
|
||||
|
||||
protected function setUpPayloadGenerator()
|
||||
protected function setUpPayloadGenerator(): void
|
||||
{
|
||||
$bootstrapper = &$this;
|
||||
|
||||
|
|
@ -149,17 +151,17 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
}
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant)
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function revert()
|
||||
public function revert(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function getPayload(string $connection)
|
||||
public function getPayload(string $connection): array
|
||||
{
|
||||
if (! tenancy()->initialized) {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -22,18 +22,21 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
|
|||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant)
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
foreach ($this->prefixedConnections() as $connection) {
|
||||
$prefix = $this->config['tenancy.redis.prefix_base'] . $tenant->getTenantKey();
|
||||
$client = Redis::connection($connection)->client();
|
||||
|
||||
$this->originalPrefixes[$connection] = $client->getOption($client::OPT_PREFIX);
|
||||
/** @var string $originalPrefix */
|
||||
$originalPrefix = $client->getOption($client::OPT_PREFIX);
|
||||
|
||||
$this->originalPrefixes[$connection] = $originalPrefix;
|
||||
$client->setOption($client::OPT_PREFIX, $prefix);
|
||||
}
|
||||
}
|
||||
|
||||
public function revert()
|
||||
public function revert(): void
|
||||
{
|
||||
foreach ($this->prefixedConnections() as $connection) {
|
||||
$client = Redis::connection($connection)->client();
|
||||
|
|
@ -44,7 +47,8 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
|
|||
$this->originalPrefixes = [];
|
||||
}
|
||||
|
||||
protected function prefixedConnections()
|
||||
/** @return string[] */
|
||||
protected function prefixedConnections(): array
|
||||
{
|
||||
return $this->config['tenancy.redis.prefixed_connections'];
|
||||
}
|
||||
|
|
|
|||
54
src/Commands/Down.php
Normal file
54
src/Commands/Down.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -8,24 +8,11 @@ use Illuminate\Console\Command;
|
|||
|
||||
class Install extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'tenancy:install';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Install stancl/tenancy.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): void
|
||||
{
|
||||
$this->comment('Installing stancl/tenancy...');
|
||||
$this->callSilent('vendor:publish', [
|
||||
|
|
|
|||
58
src/Commands/Link.php
Normal file
58
src/Commands/Link.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
|
||||
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
|
||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
||||
|
||||
class Link extends Command
|
||||
{
|
||||
use HasATenantsOption;
|
||||
|
||||
protected $signature = 'tenants:link
|
||||
{--tenants=* : The tenant(s) to run the command for. Default: all}
|
||||
{--relative : Create the symbolic link using relative paths}
|
||||
{--force : Recreate existing symbolic links}
|
||||
{--remove : Remove symbolic links}';
|
||||
|
||||
protected $description = 'Create or remove tenant symbolic links.';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$tenants = $this->getTenants();
|
||||
|
||||
try {
|
||||
if ($this->option('remove')) {
|
||||
$this->removeLinks($tenants);
|
||||
} else {
|
||||
$this->createLinks($tenants);
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function removeLinks(LazyCollection $tenants): void
|
||||
{
|
||||
RemoveStorageSymlinksAction::handle($tenants);
|
||||
|
||||
$this->info('The links have been removed.');
|
||||
}
|
||||
|
||||
protected function createLinks(LazyCollection $tenants): void
|
||||
{
|
||||
CreateStorageSymlinksAction::handle(
|
||||
$tenants,
|
||||
(bool) ($this->option('relative') ?? false),
|
||||
(bool) ($this->option('force') ?? false),
|
||||
);
|
||||
|
||||
$this->info('The links have been created.');
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ namespace Stancl\Tenancy\Commands;
|
|||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
||||
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||
|
|
@ -15,7 +14,7 @@ use Stancl\Tenancy\Events\MigratingDatabase;
|
|||
|
||||
class Migrate extends MigrateCommand
|
||||
{
|
||||
use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand;
|
||||
use HasATenantsOption, ExtendsLaravelCommand;
|
||||
|
||||
protected $description = 'Run migrations for tenant(s)';
|
||||
|
||||
|
|
@ -31,10 +30,7 @@ class Migrate extends MigrateCommand
|
|||
$this->specifyParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
foreach (config('tenancy.migration_parameters') as $parameter => $value) {
|
||||
if (! $this->input->hasParameterOption($parameter)) {
|
||||
|
|
@ -43,10 +39,10 @@ class Migrate extends MigrateCommand
|
|||
}
|
||||
|
||||
if (! $this->confirmToProceed()) {
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
||||
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
||||
|
||||
event(new MigratingDatabase($tenant));
|
||||
|
|
@ -56,5 +52,7 @@ class Migrate extends MigrateCommand
|
|||
|
||||
event(new DatabaseMigrated($tenant));
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,19 +5,13 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
final class MigrateFresh extends Command
|
||||
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)';
|
||||
|
||||
public function __construct()
|
||||
|
|
@ -29,12 +23,9 @@ final class MigrateFresh extends Command
|
|||
$this->setName('tenants:migrate-fresh');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): void
|
||||
{
|
||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
||||
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||
$this->info('Dropping tables.');
|
||||
$this->call('db:wipe', array_filter([
|
||||
'--database' => 'tenant',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ namespace Stancl\Tenancy\Commands;
|
|||
|
||||
use Illuminate\Database\Console\Migrations\RollbackCommand;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
||||
use Stancl\Tenancy\Events\DatabaseRolledBack;
|
||||
|
|
@ -14,25 +13,10 @@ use Stancl\Tenancy\Events\RollingBackDatabase;
|
|||
|
||||
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).';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Migrator $migrator)
|
||||
{
|
||||
parent::__construct($migrator);
|
||||
|
|
@ -40,10 +24,7 @@ class Rollback extends RollbackCommand
|
|||
$this->specifyTenantSignature();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
foreach (config('tenancy.migration_parameters') as $parameter => $value) {
|
||||
if (! $this->input->hasParameterOption($parameter)) {
|
||||
|
|
@ -52,10 +33,10 @@ class Rollback extends RollbackCommand
|
|||
}
|
||||
|
||||
if (! $this->confirmToProceed()) {
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
||||
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
||||
|
||||
event(new RollingBackDatabase($tenant));
|
||||
|
|
@ -65,5 +46,12 @@ class Rollback extends RollbackCommand
|
|||
|
||||
event(new DatabaseRolledBack($tenant));
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected static function getTenantCommandName(): string
|
||||
{
|
||||
return 'tenants:rollback';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,33 +6,24 @@ namespace Stancl\Tenancy\Commands;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
|
||||
class Run extends Command
|
||||
{
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
use HasATenantsOption;
|
||||
|
||||
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.}
|
||||
{--tenants=* : The tenant(s) to run the command for. Default: all}';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): void
|
||||
{
|
||||
$argvInput = $this->ArgvInput();
|
||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) use ($argvInput) {
|
||||
$argvInput = $this->argvInput();
|
||||
|
||||
tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) {
|
||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
||||
|
||||
$this->getLaravel()
|
||||
|
|
@ -41,17 +32,17 @@ class Run extends Command
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command as ArgvInput instance.
|
||||
*/
|
||||
protected function ArgvInput(): ArgvInput
|
||||
protected function argvInput(): ArgvInput
|
||||
{
|
||||
/** @var string $commandname */
|
||||
$commandname = $this->argument('commandname');
|
||||
|
||||
// Convert string command to array
|
||||
$subCommand = explode(' ', $this->argument('commandname'));
|
||||
$subcommand = explode(' ', $commandname);
|
||||
|
||||
// Add "artisan" as first parameter because ArgvInput expects "artisan" as first parameter and later removes it
|
||||
array_unshift($subCommand, 'artisan');
|
||||
array_unshift($subcommand, 'artisan');
|
||||
|
||||
return new ArgvInput($subCommand);
|
||||
return new ArgvInput($subcommand);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,29 +14,16 @@ class Seed extends SeedCommand
|
|||
{
|
||||
use HasATenantsOption;
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Seed tenant database(s).';
|
||||
|
||||
protected $name = 'tenants:seed';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ConnectionResolverInterface $resolver)
|
||||
{
|
||||
parent::__construct($resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
foreach (config('tenancy.seeder_parameters') as $parameter => $value) {
|
||||
if (! $this->input->hasParameterOption($parameter)) {
|
||||
|
|
@ -45,10 +32,10 @@ class Seed extends SeedCommand
|
|||
}
|
||||
|
||||
if (! $this->confirmToProceed()) {
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
||||
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
||||
|
||||
event(new SeedingDatabase($tenant));
|
||||
|
|
@ -58,5 +45,7 @@ class Seed extends SeedCommand
|
|||
|
||||
event(new DatabaseSeeded($tenant));
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,39 +5,28 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
class TenantList extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'tenants:list';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'List tenants.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): void
|
||||
{
|
||||
$this->info('Listing all tenants.');
|
||||
tenancy()
|
||||
->query()
|
||||
->cursor()
|
||||
->each(function (Tenant $tenant) {
|
||||
if ($tenant->domains) {
|
||||
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? []));
|
||||
} else {
|
||||
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}");
|
||||
}
|
||||
});
|
||||
|
||||
$tenants = tenancy()->query()->cursor();
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
/** @var Model&Tenant $tenant */
|
||||
if ($tenant->domains) {
|
||||
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? []));
|
||||
} else {
|
||||
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/Commands/Up.php
Normal file
27
src/Commands/Up.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,12 @@ namespace Stancl\Tenancy\Concerns;
|
|||
|
||||
trait DealsWithMigrations
|
||||
{
|
||||
protected function getMigrationPaths()
|
||||
protected function getMigrationPaths(): array
|
||||
{
|
||||
if ($this->input->hasOption('path') && $this->input->getOption('path')) {
|
||||
return parent::getMigrationPaths();
|
||||
}
|
||||
|
||||
return database_path('migrations/tenant');
|
||||
return [database_path('migrations/tenant')];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
src/Concerns/DealsWithTenantSymlinks.php
Normal file
48
src/Concerns/DealsWithTenantSymlinks.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Concerns;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
trait DealsWithTenantSymlinks
|
||||
{
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
{
|
||||
$diskUrls = config('tenancy.filesystem.url_override');
|
||||
$disks = config('tenancy.filesystem.root_override');
|
||||
$suffixBase = config('tenancy.filesystem.suffix_base');
|
||||
$tenantKey = $tenant->getTenantKey();
|
||||
|
||||
/** @var Collection<array<string, string>> $symlinks */
|
||||
$symlinks = collect([]);
|
||||
|
||||
foreach ($diskUrls as $disk => $publicPath) {
|
||||
$storagePath = str_replace('%storage_path%', $suffixBase . $tenantKey, $disks[$disk]);
|
||||
$publicPath = str_replace('%tenant_id%', (string) $tenantKey, $publicPath);
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
/** Determine if the provided path is an existing symlink. */
|
||||
protected static function symlinkExists(string $link): bool
|
||||
{
|
||||
return file_exists($link) && is_link($link);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Contracts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property-read Tenant $tenant
|
||||
*
|
||||
|
|
@ -15,5 +17,5 @@ namespace Stancl\Tenancy\Contracts;
|
|||
*/
|
||||
interface Domain
|
||||
{
|
||||
public function tenant();
|
||||
public function tenant(): BelongsTo;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace Stancl\Tenancy\Contracts;
|
|||
*/
|
||||
interface TenancyBootstrapper
|
||||
{
|
||||
public function bootstrap(Tenant $tenant);
|
||||
public function bootstrap(Tenant $tenant): void;
|
||||
|
||||
public function revert();
|
||||
public function revert(): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ abstract class TenantCannotBeCreatedException extends \Exception
|
|||
{
|
||||
abstract public function reason(): string;
|
||||
|
||||
/** @var string */
|
||||
protected $message;
|
||||
|
||||
public function __construct()
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Contracts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
32
src/Controllers/TenantAssetController.php
Normal file
32
src/Controllers/TenantAssetController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,11 +12,14 @@ use Stancl\Tenancy\Database\TenantScope;
|
|||
*/
|
||||
trait BelongsToTenant
|
||||
{
|
||||
public static $tenantIdColumn = 'tenant_id';
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(config('tenancy.tenant_model'), BelongsToTenant::$tenantIdColumn);
|
||||
return $this->belongsTo(config('tenancy.tenant_model'), static::tenantIdColumn());
|
||||
}
|
||||
|
||||
public static function tenantIdColumn(): string
|
||||
{
|
||||
return config('tenancy.single_db.tenant_id_column');
|
||||
}
|
||||
|
||||
public static function bootBelongsToTenant(): void
|
||||
|
|
@ -24,9 +27,9 @@ trait BelongsToTenant
|
|||
static::addGlobalScope(new TenantScope);
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (! $model->getAttribute(BelongsToTenant::$tenantIdColumn) && ! $model->relationLoaded('tenant')) {
|
||||
if (! $model->getAttribute(static::tenantIdColumn()) && ! $model->relationLoaded('tenant')) {
|
||||
if (tenancy()->initialized) {
|
||||
$model->setAttribute(BelongsToTenant::$tenantIdColumn, tenant()->getTenantKey());
|
||||
$model->setAttribute(static::tenantIdColumn(), tenant()->getTenantKey());
|
||||
$model->setRelation('tenant', tenant());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use Stancl\Tenancy\Contracts\Domain;
|
|||
|
||||
/**
|
||||
* @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains
|
||||
* @mixin \Illuminate\Database\Eloquent\Model
|
||||
* @mixin \Stancl\Tenancy\Contracts\Tenant
|
||||
*/
|
||||
trait HasDomains
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ trait HasScopedValidationRules
|
|||
{
|
||||
public function unique($table, $column = 'NULL')
|
||||
{
|
||||
return (new Unique($table, $column))->where(BelongsToTenant::$tenantIdColumn, $this->getTenantKey());
|
||||
return (new Unique($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey());
|
||||
}
|
||||
|
||||
public function exists($table, $column = 'NULL')
|
||||
{
|
||||
return (new Exists($table, $column))->where(BelongsToTenant::$tenantIdColumn, $this->getTenantKey());
|
||||
return (new Exists($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/Database/Concerns/InitializationHelpers.php
Normal file
21
src/Database/Concerns/InitializationHelpers.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,21 +5,15 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Resolvers;
|
||||
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
trait InvalidatesResolverCache
|
||||
{
|
||||
public static $resolvers = [
|
||||
Resolvers\DomainTenantResolver::class,
|
||||
Resolvers\PathTenantResolver::class,
|
||||
Resolvers\RequestDataTenantResolver::class,
|
||||
];
|
||||
|
||||
public static function bootInvalidatesResolverCache(): void
|
||||
{
|
||||
static::saved(function (Tenant $tenant) {
|
||||
foreach (static::$resolvers as $resolver) {
|
||||
foreach (Tenancy::cachedResolvers() as $resolver) {
|
||||
/** @var CachedTenantResolver $resolver */
|
||||
$resolver = app($resolver);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,24 +5,18 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Resolvers;
|
||||
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
/**
|
||||
* Meant to be used on models that belong to tenants.
|
||||
*/
|
||||
trait InvalidatesTenantsResolverCache
|
||||
{
|
||||
public static $resolvers = [
|
||||
Resolvers\DomainTenantResolver::class,
|
||||
Resolvers\PathTenantResolver::class,
|
||||
Resolvers\RequestDataTenantResolver::class,
|
||||
];
|
||||
|
||||
public static function bootInvalidatesTenantsResolverCache(): void
|
||||
{
|
||||
static::saved(function (Model $model) {
|
||||
foreach (static::$resolvers as $resolver) {
|
||||
foreach (Tenancy::cachedResolvers() as $resolver) {
|
||||
/** @var CachedTenantResolver $resolver */
|
||||
$resolver = app($resolver);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,27 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* @mixin \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
trait MaintenanceMode
|
||||
{
|
||||
public function putDownForMaintenance($data = [])
|
||||
public function putDownForMaintenance($data = []): void
|
||||
{
|
||||
$this->update(['maintenance_mode' => [
|
||||
'time' => $data['time'] ?? Carbon::now()->getTimestamp(),
|
||||
'message' => $data['message'] ?? null,
|
||||
'retry' => $data['retry'] ?? null,
|
||||
'allowed' => $data['allowed'] ?? [],
|
||||
]]);
|
||||
$this->update([
|
||||
'maintenance_mode' => [
|
||||
'except' => $data['except'] ?? null,
|
||||
'redirect' => $data['redirect'] ?? null,
|
||||
'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]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,5 +9,15 @@ use Stancl\Tenancy\Database\DatabaseConfig;
|
|||
|
||||
interface TenantWithDatabase extends Tenant
|
||||
{
|
||||
/** Get the tenant's database config. */
|
||||
public function database(): DatabaseConfig;
|
||||
|
||||
/** Get the internal prefix. */
|
||||
public static function internalPrefix(): string;
|
||||
|
||||
/** Get an internal key. */
|
||||
public function getInternal(string $key): mixed;
|
||||
|
||||
/** Set internal key. */
|
||||
public function setInternal(string $key, mixed $value): static;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,20 +28,20 @@ class DatabaseConfig
|
|||
|
||||
public static function __constructStatic(): void
|
||||
{
|
||||
static::$usernameGenerator = static::$usernameGenerator ?? function (Tenant $tenant) {
|
||||
static::$usernameGenerator = static::$usernameGenerator ?? function (Model&Tenant $tenant) {
|
||||
return Str::random(16);
|
||||
};
|
||||
|
||||
static::$passwordGenerator = static::$passwordGenerator ?? function (Tenant $tenant) {
|
||||
static::$passwordGenerator = static::$passwordGenerator ?? function (Model&Tenant $tenant) {
|
||||
return Hash::make(Str::random(32));
|
||||
};
|
||||
|
||||
static::$databaseNameGenerator = static::$databaseNameGenerator ?? function (Tenant $tenant) {
|
||||
static::$databaseNameGenerator = static::$databaseNameGenerator ?? function (Model&Tenant $tenant) {
|
||||
return config('tenancy.database.prefix') . $tenant->getTenantKey() . config('tenancy.database.suffix');
|
||||
};
|
||||
}
|
||||
|
||||
public function __construct(Tenant $tenant)
|
||||
public function __construct(Model&Tenant $tenant)
|
||||
{
|
||||
static::__constructStatic();
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ class DatabaseConfig
|
|||
static::$passwordGenerator = $passwordGenerator;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant);
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ class DatabaseConfig
|
|||
*/
|
||||
public function makeCredentials(): void
|
||||
{
|
||||
$this->tenant->setInternal('db_name', $this->getName() ?? (static::$databaseNameGenerator)($this->tenant));
|
||||
$this->tenant->setInternal('db_name', $this->getName());
|
||||
|
||||
if ($this->manager() instanceof Contracts\ManagesDatabaseUsers) {
|
||||
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));
|
||||
|
|
|
|||
|
|
@ -15,25 +15,14 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
|||
*/
|
||||
class DatabaseManager
|
||||
{
|
||||
/** @var Application */
|
||||
protected $app;
|
||||
|
||||
/** @var BaseDatabaseManager */
|
||||
protected $database;
|
||||
|
||||
/** @var Repository */
|
||||
protected $config;
|
||||
|
||||
public function __construct(Application $app, BaseDatabaseManager $database, Repository $config)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->database = $database;
|
||||
$this->config = $config;
|
||||
public function __construct(
|
||||
protected Application $app,
|
||||
protected BaseDatabaseManager $database,
|
||||
protected Repository $config,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a tenant's database.
|
||||
*/
|
||||
/** Connect to a tenant's database. */
|
||||
public function connectToTenant(TenantWithDatabase $tenant): void
|
||||
{
|
||||
$this->purgeTenantConnection();
|
||||
|
|
@ -41,35 +30,27 @@ class DatabaseManager
|
|||
$this->setDefaultConnection('tenant');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect to the default non-tenant connection.
|
||||
*/
|
||||
/** Reconnect to the default non-tenant connection. */
|
||||
public function reconnectToCentral(): void
|
||||
{
|
||||
$this->purgeTenantConnection();
|
||||
$this->setDefaultConnection($this->config->get('tenancy.database.central_connection'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the default database connection config.
|
||||
*/
|
||||
/** Change the default database connection config. */
|
||||
public function setDefaultConnection(string $connection): void
|
||||
{
|
||||
$this->config['database.default'] = $connection;
|
||||
$this->database->setDefaultConnection($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the tenant database connection.
|
||||
*/
|
||||
/** Create the tenant database connection. */
|
||||
public function createTenantConnection(TenantWithDatabase $tenant): void
|
||||
{
|
||||
$this->config['database.connections.tenant'] = $tenant->database()->connection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge the tenant database connection.
|
||||
*/
|
||||
/** Purge the tenant database connection. */
|
||||
public function purgeTenantConnection(): void
|
||||
{
|
||||
if (array_key_exists('tenant', $this->database->getConnections())) {
|
||||
|
|
@ -83,8 +64,8 @@ class DatabaseManager
|
|||
* Check if a tenant can be created.
|
||||
*
|
||||
* @throws TenantCannotBeCreatedException
|
||||
* @throws DatabaseManagerNotRegisteredException
|
||||
* @throws TenantDatabaseAlreadyExistsException
|
||||
* @throws Exceptions\DatabaseManagerNotRegisteredException
|
||||
* @throws Exceptions\TenantDatabaseAlreadyExistsException
|
||||
*/
|
||||
public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void
|
||||
{
|
||||
|
|
@ -94,8 +75,13 @@ class DatabaseManager
|
|||
throw new Exceptions\TenantDatabaseAlreadyExistsException($database);
|
||||
}
|
||||
|
||||
if ($manager instanceof Contracts\ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) {
|
||||
throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username);
|
||||
if ($manager instanceof Contracts\ManagesDatabaseUsers) {
|
||||
/** @var string $username */
|
||||
$username = $tenant->database()->getUsername();
|
||||
|
||||
if ($manager->userExists($username)) {
|
||||
throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class Tenant extends Model implements Contracts\Tenant
|
|||
Concerns\HasDataColumn,
|
||||
Concerns\HasInternalKeys,
|
||||
Concerns\TenantRun,
|
||||
Concerns\InitializationHelpers,
|
||||
Concerns\InvalidatesResolverCache;
|
||||
|
||||
protected $table = 'tenants';
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class ParentModelScope implements Scope
|
|||
$builder->whereHas($builder->getModel()->getRelationshipToPrimaryModel());
|
||||
}
|
||||
|
||||
public function extend(Builder $builder)
|
||||
public function extend(Builder $builder): void
|
||||
{
|
||||
$builder->macro('withoutParentModel', function (Builder $builder) {
|
||||
return $builder->withoutGlobalScope($this);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
|
|||
{
|
||||
use CreatesDatabaseUsers;
|
||||
|
||||
public static $grants = [
|
||||
/** @var string[] */
|
||||
public static array $grants = [
|
||||
'ALTER', 'ALTER ROUTINE', 'CREATE', 'CREATE ROUTINE', 'CREATE TEMPORARY TABLES', 'CREATE VIEW',
|
||||
'DELETE', 'DROP', 'EVENT', 'EXECUTE', 'INDEX', 'INSERT', 'LOCK TABLES', 'REFERENCES', 'SELECT',
|
||||
'SHOW VIEW', 'TRIGGER', 'UPDATE',
|
||||
|
|
|
|||
|
|
@ -25,11 +25,7 @@ class PostgreSQLSchemaManager extends TenantDatabaseManager
|
|||
|
||||
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
|
||||
{
|
||||
if (version_compare(app()->version(), '9.0', '>=')) {
|
||||
$baseConfig['search_path'] = $databaseName;
|
||||
} else {
|
||||
$baseConfig['schema'] = $databaseName;
|
||||
}
|
||||
$baseConfig['search_path'] = $databaseName;
|
||||
|
||||
return $baseConfig;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@ use Throwable;
|
|||
|
||||
class SQLiteDatabaseManager implements TenantDatabaseManager
|
||||
{
|
||||
/**
|
||||
* SQLite Database path without ending slash.
|
||||
*/
|
||||
public static string|null $path = null;
|
||||
|
||||
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
try {
|
||||
return file_put_contents(database_path($tenant->database()->getName()), '');
|
||||
return (bool) file_put_contents($this->getPath($tenant->database()->getName()), '');
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -22,7 +27,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
|||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
try {
|
||||
return unlink(database_path($tenant->database()->getName()));
|
||||
return unlink($this->getPath($tenant->database()->getName()));
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -30,7 +35,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
|||
|
||||
public function databaseExists(string $name): bool
|
||||
{
|
||||
return file_exists(database_path($name));
|
||||
return file_exists($this->getPath($name));
|
||||
}
|
||||
|
||||
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
|
||||
|
|
@ -44,4 +49,13 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
|||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function getPath(string $name): string
|
||||
{
|
||||
if (static::$path) {
|
||||
return static::$path . DIRECTORY_SEPARATOR . $name;
|
||||
}
|
||||
|
||||
return database_path($name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ class TenantScope implements Scope
|
|||
return;
|
||||
}
|
||||
|
||||
$builder->where($model->qualifyColumn(BelongsToTenant::$tenantIdColumn), tenant()->getTenantKey());
|
||||
$builder->where($model->qualifyColumn(BelongsToTenant::tenantIdColumn()), tenant()->getTenantKey());
|
||||
}
|
||||
|
||||
public function extend(Builder $builder)
|
||||
public function extend(Builder $builder): void
|
||||
{
|
||||
$builder->macro('withoutTenancy', function (Builder $builder) {
|
||||
return $builder->withoutGlobalScope($this);
|
||||
|
|
|
|||
|
|
@ -8,11 +8,8 @@ use Stancl\Tenancy\Tenancy;
|
|||
|
||||
abstract class TenancyEvent
|
||||
{
|
||||
/** @var Tenancy */
|
||||
public $tenancy;
|
||||
|
||||
public function __construct(Tenancy $tenancy)
|
||||
{
|
||||
$this->tenancy = $tenancy;
|
||||
public function __construct(
|
||||
public Tenancy $tenancy,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
src/Events/CreatingStorageSymlink.php
Normal file
9
src/Events/CreatingStorageSymlink.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class CreatingStorageSymlink extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
9
src/Events/RemovingStorageSymlink.php
Normal file
9
src/Events/RemovingStorageSymlink.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class RemovingStorageSymlink extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
9
src/Events/StorageSymlinkCreated.php
Normal file
9
src/Events/StorageSymlinkCreated.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class StorageSymlinkCreated extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
9
src/Events/StorageSymlinkRemoved.php
Normal file
9
src/Events/StorageSymlinkRemoved.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class StorageSymlinkRemoved extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use Exception;
|
|||
|
||||
class DomainOccupiedByOtherTenantException extends Exception
|
||||
{
|
||||
public function __construct($domain)
|
||||
public function __construct(string $domain)
|
||||
{
|
||||
parent::__construct("The $domain domain is occupied by another tenant.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class RouteIsMissingTenantParameterException extends Exception
|
|||
{
|
||||
public function __construct()
|
||||
{
|
||||
$parameter = PathTenantResolver::$tenantParameterName;
|
||||
$parameter = PathTenantResolver::tenantParameterName();
|
||||
|
||||
parent::__construct("The route's first argument is not the tenant id (configured paramter name: $parameter).");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use Exception;
|
|||
|
||||
class TenancyNotInitializedException extends Exception
|
||||
{
|
||||
public function __construct($message = '')
|
||||
public function __construct(string $message = '')
|
||||
{
|
||||
parent::__construct($message ?: 'Tenancy is not initialized.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,24 +16,25 @@ use Stancl\Tenancy\Tenancy;
|
|||
|
||||
class TenantConfig implements Feature
|
||||
{
|
||||
/** @var Repository */
|
||||
protected $config;
|
||||
|
||||
public array $originalConfig = [];
|
||||
|
||||
public static $storageToConfigMap = [
|
||||
/** @var array<string, string|array> */
|
||||
public static array $storageToConfigMap = [
|
||||
// 'paypal_api_key' => 'services.paypal.api_key',
|
||||
];
|
||||
|
||||
public function __construct(Repository $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
public function __construct(
|
||||
protected Repository $config,
|
||||
) {
|
||||
}
|
||||
|
||||
public function bootstrap(Tenancy $tenancy): void
|
||||
{
|
||||
Event::listen(TenancyBootstrapped::class, function (TenancyBootstrapped $event) {
|
||||
$this->setTenantConfig($event->tenancy->tenant);
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = $event->tenancy->tenant;
|
||||
|
||||
$this->setTenantConfig($tenant);
|
||||
});
|
||||
|
||||
Event::listen(RevertedToCentralContext::class, function () {
|
||||
|
|
@ -43,8 +44,8 @@ class TenantConfig implements Feature
|
|||
|
||||
public function setTenantConfig(Tenant $tenant): void
|
||||
{
|
||||
/** @var Tenant|Model $tenant */
|
||||
foreach (static::$storageToConfigMap as $storageKey => $configKey) {
|
||||
/** @var Tenant&Model $tenant */
|
||||
$override = Arr::get($tenant, $storageKey);
|
||||
|
||||
if (! is_null($override)) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class UniversalRoutes implements Feature
|
|||
public static string $middlewareGroup = 'universal';
|
||||
|
||||
// todo docblock
|
||||
/** @var array<class-string<\Stancl\Tenancy\Middleware\IdentificationMiddleware>> */
|
||||
public static array $identificationMiddlewares = [
|
||||
Middleware\InitializeTenancyByDomain::class,
|
||||
Middleware\InitializeTenancyBySubdomain::class,
|
||||
|
|
@ -42,7 +43,10 @@ class UniversalRoutes implements Feature
|
|||
|
||||
public static function routeHasMiddleware(Route $route, string $middleware): bool
|
||||
{
|
||||
if (in_array($middleware, $route->middleware(), true)) {
|
||||
/** @var array $routeMiddleware */
|
||||
$routeMiddleware = $route->middleware();
|
||||
|
||||
if (in_array($middleware, $routeMiddleware, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class CreateDatabase implements ShouldQueue
|
|||
) {
|
||||
}
|
||||
|
||||
public function handle(DatabaseManager $databaseManager)
|
||||
public function handle(DatabaseManager $databaseManager): bool
|
||||
{
|
||||
event(new CreatingDatabase($this->tenant));
|
||||
|
||||
|
|
@ -38,5 +38,7 @@ class CreateDatabase implements ShouldQueue
|
|||
$this->tenant->database()->hostManager()->createDatabase($this->tenant);
|
||||
|
||||
event(new DatabaseCreated($this->tenant));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
src/Jobs/CreateStorageSymlinks.php
Normal file
28
src/Jobs/CreateStorageSymlinks.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
class CreateStorageSymlinks implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public Tenant $tenant,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
CreateStorageSymlinksAction::handle($this->tenant);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,14 +9,12 @@ use Illuminate\Database\Eloquent\Model;
|
|||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
|
||||
class DeleteDomains
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** @var TenantWithDatabase&Model&HasDomains */ // todo unresolvable type for phpstan
|
||||
protected TenantWithDatabase&Model $tenant;
|
||||
|
||||
public function __construct(TenantWithDatabase&Model $tenant)
|
||||
|
|
|
|||
40
src/Jobs/RemoveStorageSymlinks.php
Normal file
40
src/Jobs/RemoveStorageSymlinks.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
class RemoveStorageSymlinks implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public Tenant $tenant;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
RemoveStorageSymlinksAction::handle($this->tenant);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Events\BootstrappingTenancy;
|
||||
use Stancl\Tenancy\Events\TenancyBootstrapped;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
|
|
@ -15,7 +16,10 @@ class BootstrapTenancy
|
|||
event(new BootstrappingTenancy($event->tenancy));
|
||||
|
||||
foreach ($event->tenancy->getBootstrappers() as $bootstrapper) {
|
||||
$bootstrapper->bootstrap($event->tenancy->tenant);
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = $event->tenancy->tenant;
|
||||
|
||||
$bootstrapper->bootstrap($tenant);
|
||||
}
|
||||
|
||||
event(new TenancyBootstrapped($event->tenancy));
|
||||
|
|
|
|||
|
|
@ -4,21 +4,22 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\Contracts\TenantEvent;
|
||||
|
||||
class CreateTenantConnection
|
||||
{
|
||||
/** @var DatabaseManager */
|
||||
protected $database;
|
||||
|
||||
public function __construct(DatabaseManager $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
public function __construct(
|
||||
protected DatabaseManager $database,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(TenantEvent $event): void
|
||||
{
|
||||
$this->database->createTenantConnection($event->tenant);
|
||||
/** @var TenantWithDatabase */
|
||||
$tenant = $event->tenant;
|
||||
|
||||
$this->database->createTenantConnection($tenant);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
src/Listeners/DeleteTenantStorage.php
Normal file
16
src/Listeners/DeleteTenantStorage.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Stancl\Tenancy\Events\DeletingTenant;
|
||||
|
||||
class DeleteTenantStorage
|
||||
{
|
||||
public function handle(DeletingTenant $event): void
|
||||
{
|
||||
File::deleteDirectory($event->tenant->run(fn () => storage_path()));
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ abstract class QueueableListener implements ShouldQueue
|
|||
{
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
public function shouldQueue($event): bool
|
||||
public function shouldQueue(object $event): bool
|
||||
{
|
||||
if (static::$shouldQueue) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ namespace Stancl\Tenancy\Middleware;
|
|||
use Closure;
|
||||
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
||||
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
|
||||
|
|
@ -21,19 +20,38 @@ class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
|
|||
if (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);
|
||||
}
|
||||
|
||||
if ($this->inExceptArray($request)) {
|
||||
return $next($request);
|
||||
if (isset($data['redirect'])) {
|
||||
$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(
|
||||
503,
|
||||
(int) ($data['status'] ?? 503),
|
||||
'Service Unavailable',
|
||||
null,
|
||||
isset($data['retry']) ? ['Retry-After' => $data['retry']] : []
|
||||
$this->getHeaders($data)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,22 +4,22 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
/**
|
||||
* @property Tenancy $tenancy
|
||||
* @property TenantResolver $resolver
|
||||
*/
|
||||
abstract class IdentificationMiddleware
|
||||
{
|
||||
/** @var callable */
|
||||
public static $onFail;
|
||||
public static ?Closure $onFail = null;
|
||||
|
||||
/** @var Tenancy */
|
||||
protected $tenancy;
|
||||
|
||||
/** @var TenantResolver */
|
||||
protected $resolver;
|
||||
|
||||
public function initializeTenancy($request, $next, ...$resolverArguments)
|
||||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function initializeTenancy(Request $request, Closure $next, mixed ...$resolverArguments): mixed
|
||||
{
|
||||
try {
|
||||
$this->tenancy->initialize(
|
||||
|
|
|
|||
|
|
@ -5,32 +5,22 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
class InitializeTenancyByDomain extends IdentificationMiddleware
|
||||
{
|
||||
/** @var callable|null */
|
||||
public static $onFail;
|
||||
public static ?Closure $onFail = null;
|
||||
|
||||
/** @var Tenancy */
|
||||
protected $tenancy;
|
||||
|
||||
/** @var DomainTenantResolver */
|
||||
protected $resolver;
|
||||
|
||||
public function __construct(Tenancy $tenancy, DomainTenantResolver $resolver)
|
||||
{
|
||||
$this->tenancy = $tenancy;
|
||||
$this->resolver = $resolver;
|
||||
public function __construct(
|
||||
protected Tenancy $tenancy,
|
||||
protected DomainTenantResolver $resolver,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
return $this->initializeTenancy(
|
||||
$request,
|
||||
|
|
|
|||
|
|
@ -5,16 +5,13 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class InitializeTenancyByDomainOrSubdomain
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if ($this->isSubdomain($request->getHost())) {
|
||||
return app(InitializeTenancyBySubdomain::class)->handle($request, $next);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Events\InitializingTenancy;
|
||||
use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException;
|
||||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||
|
|
@ -16,22 +17,16 @@ use Stancl\Tenancy\Tenancy;
|
|||
|
||||
class InitializeTenancyByPath extends IdentificationMiddleware
|
||||
{
|
||||
/** @var callable|null */
|
||||
public static $onFail;
|
||||
public static ?Closure $onFail = null;
|
||||
|
||||
/** @var Tenancy */
|
||||
protected $tenancy;
|
||||
|
||||
/** @var PathTenantResolver */
|
||||
protected $resolver;
|
||||
|
||||
public function __construct(Tenancy $tenancy, PathTenantResolver $resolver)
|
||||
{
|
||||
$this->tenancy = $tenancy;
|
||||
$this->resolver = $resolver;
|
||||
public function __construct(
|
||||
protected Tenancy $tenancy,
|
||||
protected PathTenantResolver $resolver,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
/** @var Route $route */
|
||||
$route = $request->route();
|
||||
|
|
@ -39,11 +34,8 @@ class InitializeTenancyByPath extends IdentificationMiddleware
|
|||
// Only initialize tenancy if tenant is the first parameter
|
||||
// We don't want to initialize tenancy if the tenant is
|
||||
// simply injected into some route controller action.
|
||||
if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) {
|
||||
// Set tenant as a default parameter for the URLs in the current request
|
||||
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
|
||||
URL::defaults([PathTenantResolver::$tenantParameterName => $event->tenancy->tenant->getTenantKey()]);
|
||||
});
|
||||
if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) {
|
||||
$this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized();
|
||||
|
||||
return $this->initializeTenancy(
|
||||
$request,
|
||||
|
|
@ -56,4 +48,16 @@ class InitializeTenancyByPath extends IdentificationMiddleware
|
|||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void
|
||||
{
|
||||
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = $event->tenancy->tenant;
|
||||
|
||||
URL::defaults([
|
||||
PathTenantResolver::tenantParameterName() => $tenant->getTenantKey(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,33 +11,18 @@ use Stancl\Tenancy\Tenancy;
|
|||
|
||||
class InitializeTenancyByRequestData extends IdentificationMiddleware
|
||||
{
|
||||
/** @var string|null */
|
||||
public static $header = 'X-Tenant';
|
||||
public static string $header = 'X-Tenant';
|
||||
public static string $queryParameter = 'tenant';
|
||||
public static ?Closure $onFail = null;
|
||||
|
||||
/** @var string|null */
|
||||
public static $queryParameter = 'tenant';
|
||||
|
||||
/** @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;
|
||||
public function __construct(
|
||||
protected Tenancy $tenancy,
|
||||
protected RequestDataTenantResolver $resolver,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if ($request->method() !== 'OPTIONS') {
|
||||
return $this->initializeTenancy($request, $next, $this->getPayload($request));
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Middleware;
|
|||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Exceptions\NotASubdomainException;
|
||||
|
|
@ -21,15 +22,10 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
|||
*/
|
||||
public static $subdomainIndex = 0;
|
||||
|
||||
/** @var callable|null */
|
||||
public static $onFail;
|
||||
public static ?Closure $onFail = null;
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
/** @return Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
$subdomain = $this->makeSubdomain($request->getHost());
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@ class PreventAccessFromCentralDomains
|
|||
{
|
||||
/**
|
||||
* 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'))) {
|
||||
$abortRequest = static::$abortRequest ?? function () {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
|||
|
||||
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) {
|
||||
throw new TenancyNotInitializedException('Tenancy needs to be initialized before the session scoping middleware is executed');
|
||||
|
|
|
|||
|
|
@ -11,23 +11,17 @@ use Stancl\Tenancy\Contracts\TenantResolver;
|
|||
|
||||
abstract class CachedTenantResolver implements TenantResolver
|
||||
{
|
||||
public static bool $shouldCache = false; // todo docblocks for these
|
||||
|
||||
public static int $cacheTTL = 3600; // seconds
|
||||
|
||||
public static string|null $cacheStore = null; // default
|
||||
|
||||
/** @var Repository */
|
||||
protected $cache;
|
||||
|
||||
public function __construct(Factory $cache)
|
||||
{
|
||||
$this->cache = $cache->store(static::$cacheStore);
|
||||
$this->cache = $cache->store(static::cacheStore());
|
||||
}
|
||||
|
||||
public function resolve(mixed ...$args): Tenant
|
||||
{
|
||||
if (! static::$shouldCache) {
|
||||
if (! static::shouldCache()) {
|
||||
return $this->resolveWithoutCache(...$args);
|
||||
}
|
||||
|
||||
|
|
@ -42,14 +36,14 @@ abstract class CachedTenantResolver implements TenantResolver
|
|||
}
|
||||
|
||||
$tenant = $this->resolveWithoutCache(...$args);
|
||||
$this->cache->put($key, $tenant, static::$cacheTTL);
|
||||
$this->cache->put($key, $tenant, static::cacheTTL());
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
public function invalidateCache(Tenant $tenant): void
|
||||
{
|
||||
if (! static::$shouldCache) {
|
||||
if (! static::shouldCache()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +59,7 @@ abstract class CachedTenantResolver implements TenantResolver
|
|||
|
||||
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[]
|
||||
*/
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,17 +14,10 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
|
|||
/** The model representing the domain that the tenant was identified on. */
|
||||
public static Domain $currentDomain; // todo |null?
|
||||
|
||||
public static bool $shouldCache = false;
|
||||
|
||||
public static int $cacheTTL = 3600; // seconds
|
||||
|
||||
public static string|null $cacheStore = null; // default
|
||||
|
||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
||||
{
|
||||
$domain = $args[0];
|
||||
|
||||
/** @var Tenant|null $tenant */
|
||||
$tenant = config('tenancy.tenant_model')::query()
|
||||
->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain))
|
||||
->with('domains')
|
||||
|
|
@ -39,7 +32,7 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
|
|||
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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,21 +10,13 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
|
|||
|
||||
class PathTenantResolver extends Contracts\CachedTenantResolver
|
||||
{
|
||||
public static string $tenantParameterName = 'tenant';
|
||||
|
||||
public static bool $shouldCache = false;
|
||||
|
||||
public static int $cacheTTL = 3600; // seconds
|
||||
|
||||
public static string|null $cacheStore = null; // default
|
||||
|
||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
||||
{
|
||||
/** @var Route $route */
|
||||
$route = $args[0];
|
||||
|
||||
if ($id = $route->parameter(static::$tenantParameterName)) {
|
||||
$route->forgetParameter(static::$tenantParameterName);
|
||||
if ($id = (string) $route->parameter(static::tenantParameterName())) {
|
||||
$route->forgetParameter(static::tenantParameterName());
|
||||
|
||||
if ($tenant = tenancy()->find($id)) {
|
||||
return $tenant;
|
||||
|
|
@ -37,7 +29,12 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
|||
public function getArgsForTenant(Tenant $tenant): array
|
||||
{
|
||||
return [
|
||||
[$tenant->id],
|
||||
[$tenant->getTenantKey()],
|
||||
];
|
||||
}
|
||||
|
||||
public static function tenantParameterName(): string
|
||||
{
|
||||
return config('tenancy.identification.resolvers.' . static::class . '.tenant_parameter_name') ?? 'tenant';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class RequestDataTenantResolver extends Contracts\CachedTenantResolver
|
|||
|
||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
||||
{
|
||||
$payload = $args[0];
|
||||
$payload = (string) $args[0];
|
||||
|
||||
if ($payload && $tenant = tenancy()->find($payload)) {
|
||||
return $tenant;
|
||||
|
|
@ -29,7 +29,7 @@ class RequestDataTenantResolver extends Contracts\CachedTenantResolver
|
|||
public function getArgsForTenant(Tenant $tenant): array
|
||||
{
|
||||
return [
|
||||
[$tenant->id],
|
||||
[$tenant->getTenantKey()],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class Tenancy
|
|||
}
|
||||
}
|
||||
|
||||
// todo0 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property
|
||||
// todo1 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property
|
||||
if ($this->initialized && $this->tenant->getTenantKey() === $tenant->getTenantKey()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -99,19 +99,30 @@ class Tenancy
|
|||
{
|
||||
$class = config('tenancy.tenant_model');
|
||||
|
||||
return new $class;
|
||||
/** @var Tenant&Model $model */
|
||||
$model = new $class;
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find a tenant using an ID.
|
||||
*
|
||||
* @return (Tenant&Model)|null
|
||||
*/
|
||||
public static function find(int|string $id): Tenant|null
|
||||
{
|
||||
return static::model()->where(static::model()->getTenantKeyName(), $id)->first();
|
||||
/** @var (Tenant&Model)|null */
|
||||
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback in the central context.
|
||||
* Atomic, safely reverts to previous context.
|
||||
*/
|
||||
public function central(Closure $callback)
|
||||
public function central(Closure $callback): mixed
|
||||
{
|
||||
$previousTenant = $this->tenant;
|
||||
|
||||
|
|
@ -132,7 +143,7 @@ class Tenancy
|
|||
* Run a callback for multiple tenants.
|
||||
* More performant than running $tenant->run() one by one.
|
||||
*
|
||||
* @param Tenant[]|\Traversable|string[]|null $tenants
|
||||
* @param array<Tenant>|array<string|int>|\Traversable|string|int|null $tenants
|
||||
*/
|
||||
public function runForMultiple($tenants, Closure $callback): void
|
||||
{
|
||||
|
|
@ -146,7 +157,7 @@ class Tenancy
|
|||
$tenants = is_string($tenants) ? [$tenants] : $tenants;
|
||||
|
||||
// Use all tenants if $tenants is 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;
|
||||
|
||||
|
|
@ -155,6 +166,7 @@ class Tenancy
|
|||
$tenant = $this->find($tenant);
|
||||
}
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$this->initialize($tenant);
|
||||
$callback($tenant);
|
||||
}
|
||||
|
|
@ -165,4 +177,41 @@ class Tenancy
|
|||
$this->end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached tenant resolvers used by the package.
|
||||
*
|
||||
* @return array<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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
{
|
||||
$this->commands([
|
||||
Commands\Run::class,
|
||||
Commands\Link::class,
|
||||
Commands\Seed::class,
|
||||
Commands\Install::class,
|
||||
Commands\Migrate::class,
|
||||
|
|
@ -85,6 +86,8 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
Commands\TenantList::class,
|
||||
Commands\TenantDump::class,
|
||||
Commands\MigrateFresh::class,
|
||||
Commands\Down::class,
|
||||
Commands\Up::class,
|
||||
]);
|
||||
|
||||
$this->publishes([
|
||||
|
|
@ -117,7 +120,7 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
if ($event instanceof TenancyEvent) {
|
||||
match (tenancy()->logMode()) {
|
||||
LogMode::SILENT => tenancy()->logEvent($event),
|
||||
LogMode::INSTANT => dump($event), // todo0 perhaps still log
|
||||
LogMode::INSTANT => dump($event), // todo1 perhaps still log
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
|||
|
||||
class UUIDGenerator implements UniqueIdentifierGenerator
|
||||
{
|
||||
public static function generate($resource): string
|
||||
public static function generate(Model $model): string
|
||||
{
|
||||
return Uuid::uuid4()->toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ if (! function_exists('tenant')) {
|
|||
|
||||
if (! function_exists('tenant_asset')) {
|
||||
// todo docblock
|
||||
// todo add an option to generate paths respecting the ASSET_URL
|
||||
function tenant_asset(string|null $asset): string
|
||||
{
|
||||
return route('stancl.tenancy.asset', ['path' => $asset]);
|
||||
|
|
@ -42,16 +43,42 @@ if (! function_exists('tenant_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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
69
tests/ActionTest.php
Normal file
69
tests/ActionTest.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
|
||||
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
|
||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
|
||||
beforeEach(function () {
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
||||
// todo move these to be in the same file as the other tests from this PR (#909) rather than generic "action tests"
|
||||
|
||||
test('create storage symlinks action works', function() {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::create();
|
||||
$tenantKey = $tenant->getTenantKey();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$this->assertDirectoryDoesNotExist($publicPath = public_path("public-$tenantKey"));
|
||||
|
||||
CreateStorageSymlinksAction::handle($tenant);
|
||||
|
||||
$this->assertDirectoryExists($publicPath);
|
||||
$this->assertEquals(storage_path("app/public/"), readlink($publicPath));
|
||||
});
|
||||
|
||||
test('remove storage symlinks action works', function() {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::create();
|
||||
$tenantKey = $tenant->getTenantKey();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
CreateStorageSymlinksAction::handle($tenant);
|
||||
|
||||
$this->assertDirectoryExists($publicPath = public_path("public-$tenantKey"));
|
||||
|
||||
RemoveStorageSymlinksAction::handle($tenant);
|
||||
|
||||
$this->assertDirectoryDoesNotExist($publicPath);
|
||||
});
|
||||
|
|
@ -107,12 +107,12 @@ function contextIsSwitchedWhenTenancyInitialized()
|
|||
|
||||
class MyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant)
|
||||
public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void
|
||||
{
|
||||
app()->instance('tenancy_initialized_for_tenant', $tenant->getTenantKey());
|
||||
}
|
||||
|
||||
public function revert()
|
||||
public function revert(): void
|
||||
{
|
||||
app()->instance('tenancy_ended', true);
|
||||
}
|
||||
|
|
|
|||
44
tests/BatchTest.php
Normal file
44
tests/BatchTest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Bus\BatchRepository;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
BatchTenancyBootstrapper::class,
|
||||
],
|
||||
]);
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
||||
test('batch repository is set to tenant connection and reverted', function () {
|
||||
$tenant = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
expect(getBatchRepositoryConnectionName())->toBe('central');
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
expect(getBatchRepositoryConnectionName())->toBe('tenant');
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
expect(getBatchRepositoryConnectionName())->toBe('tenant');
|
||||
|
||||
tenancy()->end();
|
||||
expect(getBatchRepositoryConnectionName())->toBe('central');
|
||||
});
|
||||
|
||||
function getBatchRepositoryConnectionName()
|
||||
{
|
||||
return app(BatchRepository::class)->getConnection()->getName();
|
||||
}
|
||||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
|
@ -14,8 +14,14 @@ use Illuminate\Support\Facades\Storage;
|
|||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Events\TenantDeleted;
|
||||
use Stancl\Tenancy\Events\DeletingTenant;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Jobs\CreateStorageSymlinks;
|
||||
use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||
|
|
@ -184,24 +190,156 @@ test('filesystem data is separated', function () {
|
|||
expect($new_storage_path)->toEqual($expected_storage_path);
|
||||
});
|
||||
|
||||
test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function () {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
$tenant1 = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
$tenant1StorageUrl = 'http://localhost/public-' . $tenant1->getKey().'/';
|
||||
$tenant2StorageUrl = 'http://localhost/public-' . $tenant2->getKey().'/';
|
||||
|
||||
tenancy()->initialize($tenant1);
|
||||
|
||||
$this->assertEquals(
|
||||
$tenant1StorageUrl,
|
||||
Storage::disk('public')->url('')
|
||||
);
|
||||
|
||||
Storage::disk('public')->put($tenant1FileName = 'tenant1.txt', 'text');
|
||||
|
||||
$this->assertEquals(
|
||||
$tenant1StorageUrl . $tenant1FileName,
|
||||
Storage::disk('public')->url($tenant1FileName)
|
||||
);
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
||||
$this->assertEquals(
|
||||
$tenant2StorageUrl,
|
||||
Storage::disk('public')->url('')
|
||||
);
|
||||
|
||||
Storage::disk('public')->put($tenant2FileName = 'tenant2.txt', 'text');
|
||||
|
||||
$this->assertEquals(
|
||||
$tenant2StorageUrl . $tenant2FileName,
|
||||
Storage::disk('public')->url($tenant2FileName)
|
||||
);
|
||||
});
|
||||
|
||||
test('files can get fetched using the storage url', function() {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
$tenant1 = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
pest()->artisan('tenants:link');
|
||||
|
||||
// First tenant
|
||||
tenancy()->initialize($tenant1);
|
||||
Storage::disk('public')->put($tenantFileName = 'tenant1.txt', $tenantKey = $tenant1->getTenantKey());
|
||||
|
||||
$url = Storage::disk('public')->url($tenantFileName);
|
||||
$tenantDiskName = Str::of(config('tenancy.filesystem.url_override.public'))->replace('%tenant_id%', $tenantKey);
|
||||
$hostname = Str::of($url)->before($tenantDiskName);
|
||||
$parsedUrl = Str::of($url)->after($hostname);
|
||||
|
||||
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
||||
|
||||
// Second tenant
|
||||
tenancy()->initialize($tenant2);
|
||||
Storage::disk('public')->put($tenantFileName = 'tenant2.txt', $tenantKey = $tenant2->getTenantKey());
|
||||
|
||||
$url = Storage::disk('public')->url($tenantFileName);
|
||||
$tenantDiskName = Str::of(config('tenancy.filesystem.url_override.public'))->replace('%tenant_id%', $tenantKey);
|
||||
$hostname = Str::of($url)->before($tenantDiskName);
|
||||
$parsedUrl = Str::of($url)->after($hostname);
|
||||
|
||||
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
||||
});
|
||||
|
||||
test('create and delete storage symlinks jobs work', function() {
|
||||
Event::listen(
|
||||
TenantCreated::class,
|
||||
JobPipeline::make([CreateStorageSymlinks::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener()
|
||||
);
|
||||
|
||||
Event::listen(
|
||||
TenantDeleted::class,
|
||||
JobPipeline::make([RemoveStorageSymlinks::class])->send(function (TenantDeleted $event) {
|
||||
return $event->tenant;
|
||||
})->toListener()
|
||||
);
|
||||
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$tenantKey = $tenant->getTenantKey();
|
||||
|
||||
$this->assertDirectoryExists(storage_path("app/public"));
|
||||
$this->assertEquals(storage_path("app/public/"), readlink(public_path("public-$tenantKey")));
|
||||
|
||||
$tenant->delete();
|
||||
|
||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
|
||||
});
|
||||
|
||||
test('local storage public urls are generated correctly', function() {
|
||||
Event::listen(DeletingTenant::class, DeleteTenantStorage::class);
|
||||
|
||||
tenancy()->initialize(Tenant::create());
|
||||
$tenantStoragePath = storage_path();
|
||||
|
||||
Storage::fake('test');
|
||||
|
||||
expect(File::isDirectory($tenantStoragePath))->toBeTrue();
|
||||
|
||||
Storage::put('test.txt', 'testing file');
|
||||
|
||||
tenant()->delete();
|
||||
|
||||
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
|
||||
});
|
||||
|
||||
function getDiskPrefix(string $disk): string
|
||||
{
|
||||
/** @var FilesystemAdapter $disk */
|
||||
$disk = Storage::disk($disk);
|
||||
$adapter = $disk->getAdapter();
|
||||
|
||||
if (! Str::startsWith(app()->version(), '9.')) {
|
||||
return $adapter->getPathPrefix();
|
||||
}
|
||||
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
|
||||
$prefixer->setAccessible(true);
|
||||
|
||||
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
|
||||
$prefixer->setAccessible(true);
|
||||
// reflection -> instance
|
||||
$prefixer = $prefixer->getValue($adapter);
|
||||
|
||||
// reflection -> instance
|
||||
$prefixer = $prefixer->getValue($adapter);
|
||||
$prefix = (new ReflectionProperty($prefixer, 'prefix'));
|
||||
$prefix->setAccessible(true);
|
||||
|
||||
$prefix = (new ReflectionProperty($prefixer, 'prefix'));
|
||||
$prefix->setAccessible(true);
|
||||
|
||||
return $prefix->getValue($prefixer);
|
||||
return $prefix->getValue($prefixer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ use Illuminate\Support\Facades\DB;
|
|||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
afterEach(function () {
|
||||
DomainTenantResolver::$shouldCache = false;
|
||||
});
|
||||
// todo@v4 test this with other resolvers as well?
|
||||
|
||||
test('tenants can be resolved using the cached resolver', function () {
|
||||
$tenant = Tenant::create();
|
||||
|
|
@ -27,14 +25,14 @@ test('the underlying resolver is not touched when using the cached resolver', fu
|
|||
|
||||
DB::enableQueryLog();
|
||||
|
||||
DomainTenantResolver::$shouldCache = false;
|
||||
config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => false]);
|
||||
|
||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
||||
DB::flushQueryLog();
|
||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
||||
pest()->assertNotEmpty(DB::getQueryLog()); // not empty
|
||||
|
||||
DomainTenantResolver::$shouldCache = true;
|
||||
config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
|
||||
|
||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
||||
DB::flushQueryLog();
|
||||
|
|
@ -50,7 +48,7 @@ test('cache is invalidated when the tenant is updated', function () {
|
|||
|
||||
DB::enableQueryLog();
|
||||
|
||||
DomainTenantResolver::$shouldCache = true;
|
||||
config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
|
||||
|
||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
||||
DB::flushQueryLog();
|
||||
|
|
@ -74,7 +72,7 @@ test('cache is invalidated when a tenants domain is changed', function () {
|
|||
|
||||
DB::enableQueryLog();
|
||||
|
||||
DomainTenantResolver::$shouldCache = true;
|
||||
config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
|
||||
|
||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
||||
DB::flushQueryLog();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
|
@ -16,6 +18,7 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
|||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Stancl\Tenancy\Tests\Etc\TestSeeder;
|
||||
use Stancl\Tenancy\Tests\Etc\User;
|
||||
|
||||
beforeEach(function () {
|
||||
|
|
@ -27,6 +30,15 @@ beforeEach(function () {
|
|||
DatabaseTenancyBootstrapper::class,
|
||||
]]);
|
||||
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
|
@ -41,9 +53,9 @@ afterEach(function () {
|
|||
test('migrate command doesnt change the db connection', function () {
|
||||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
|
||||
$old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
|
||||
$old_connection_name = app(DatabaseManager::class)->connection()->getName();
|
||||
Artisan::call('tenants:migrate');
|
||||
$new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
|
||||
$new_connection_name = app(DatabaseManager::class)->connection()->getName();
|
||||
|
||||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
expect($new_connection_name)->toEqual($old_connection_name);
|
||||
|
|
@ -116,8 +128,22 @@ test('rollback command works', function () {
|
|||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
});
|
||||
|
||||
// Incomplete test
|
||||
test('seed command works');
|
||||
test('seed command works', function (){
|
||||
$tenant = Tenant::create();
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
$tenant->run(function (){
|
||||
expect(DB::table('users')->count())->toBe(0);
|
||||
});
|
||||
|
||||
Artisan::call('tenants:seed', ['--class' => TestSeeder::class]);
|
||||
|
||||
$tenant->run(function (){
|
||||
$user = DB::table('users');
|
||||
expect($user->count())->toBe(1)
|
||||
->and($user->first()->email)->toBe('seeded@user');
|
||||
});
|
||||
});
|
||||
|
||||
test('database connection is switched to default', function () {
|
||||
databaseConnectionSwitchedToDefault();
|
||||
|
|
@ -176,8 +202,46 @@ test('run command with array of tenants works', function () {
|
|||
Artisan::call('tenants:migrate-fresh');
|
||||
|
||||
pest()->artisan("tenants:run --tenants=$tenantId1 --tenants=$tenantId2 'foo foo --b=bar --c=xyz'")
|
||||
->expectsOutput('Tenant: ' . $tenantId1)
|
||||
->expectsOutput('Tenant: ' . $tenantId2);
|
||||
->expectsOutputToContain('Tenant: ' . $tenantId1)
|
||||
->expectsOutputToContain('Tenant: ' . $tenantId2)
|
||||
->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('link command works', function() {
|
||||
$tenantId1 = Tenant::create()->getTenantKey();
|
||||
$tenantId2 = Tenant::create()->getTenantKey();
|
||||
pest()->artisan('tenants:link');
|
||||
|
||||
$this->assertDirectoryExists(storage_path("tenant-$tenantId1/app/public"));
|
||||
$this->assertEquals(storage_path("tenant-$tenantId1/app/public/"), readlink(public_path("public-$tenantId1")));
|
||||
|
||||
$this->assertDirectoryExists(storage_path("tenant-$tenantId2/app/public"));
|
||||
$this->assertEquals(storage_path("tenant-$tenantId2/app/public/"), readlink(public_path("public-$tenantId2")));
|
||||
|
||||
pest()->artisan('tenants:link', [
|
||||
'--remove' => true,
|
||||
]);
|
||||
|
||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantId1"));
|
||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantId2"));
|
||||
});
|
||||
|
||||
test('link command works with a specified tenant', function() {
|
||||
$tenantKey = Tenant::create()->getTenantKey();
|
||||
|
||||
pest()->artisan('tenants:link', [
|
||||
'--tenants' => [$tenantKey],
|
||||
]);
|
||||
|
||||
$this->assertDirectoryExists(storage_path("tenant-$tenantKey/app/public"));
|
||||
$this->assertEquals(storage_path("tenant-$tenantKey/app/public/"), readlink(public_path("public-$tenantKey")));
|
||||
|
||||
pest()->artisan('tenants:link', [
|
||||
'--remove' => true,
|
||||
'--tenants' => [$tenantKey],
|
||||
]);
|
||||
|
||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
|
||||
});
|
||||
|
||||
test('run command works when sub command asks questions and accepts arguments', function () {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ test('tenant can be identified by domain', function () {
|
|||
|
||||
test('onfail logic can be customized', function () {
|
||||
InitializeTenancyByDomain::$onFail = function () {
|
||||
return 'foo';
|
||||
return response('foo');
|
||||
};
|
||||
|
||||
pest()
|
||||
|
|
|
|||
|
|
@ -7,9 +7,13 @@ namespace Stancl\Tenancy\Tests\Etc;
|
|||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\Database\Concerns\HasDatabase;
|
||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
||||
use Stancl\Tenancy\Database\Models;
|
||||
|
||||
/**
|
||||
* @method static static create(array $attributes = [])
|
||||
*/
|
||||
class Tenant extends Models\Tenant implements TenantWithDatabase
|
||||
{
|
||||
use HasDatabase, HasDomains;
|
||||
use HasDatabase, HasDomains, MaintenanceMode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,3 +50,17 @@ test('global cache manager stores data in global cache', function () {
|
|||
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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
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 () {
|
||||
return 'bar';
|
||||
})->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
|
||||
|
|
@ -19,16 +19,40 @@ test('tenant can be in maintenance mode', function () {
|
|||
'domain' => 'acme.localhost',
|
||||
]);
|
||||
|
||||
pest()->get('http://acme.localhost/foo')
|
||||
->assertSuccessful();
|
||||
|
||||
tenancy()->end(); // flush stored tenant instance
|
||||
pest()->get('http://acme.localhost/foo')->assertStatus(200);
|
||||
|
||||
$tenant->putDownForMaintenance();
|
||||
|
||||
pest()->expectException(HttpException::class);
|
||||
pest()->withoutExceptionHandling()
|
||||
->get('http://acme.localhost/foo');
|
||||
tenancy()->end(); // End tenancy before making a request
|
||||
pest()->get('http://acme.localhost/foo')->assertStatus(503);
|
||||
|
||||
$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
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
|||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
beforeEach(function () {
|
||||
PathTenantResolver::$tenantParameterName = 'tenant';
|
||||
|
||||
Route::group([
|
||||
'prefix' => '/{tenant}',
|
||||
'middleware' => InitializeTenancyByPath::class,
|
||||
|
|
@ -26,11 +24,6 @@ beforeEach(function () {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Global state cleanup
|
||||
PathTenantResolver::$tenantParameterName = 'tenant';
|
||||
});
|
||||
|
||||
test('tenant can be identified by path', function () {
|
||||
Tenant::create([
|
||||
'id' => 'acme',
|
||||
|
|
@ -71,7 +64,7 @@ test('exception is thrown when tenant cannot be identified by path', function ()
|
|||
|
||||
test('onfail logic can be customized', function () {
|
||||
InitializeTenancyByPath::$onFail = function () {
|
||||
return 'foo';
|
||||
return response('foo');
|
||||
};
|
||||
|
||||
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 () {
|
||||
PathTenantResolver::$tenantParameterName = 'team';
|
||||
config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_parameter_name' => 'team']);
|
||||
|
||||
Route::group([
|
||||
'prefix' => '/{team}',
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ test('header identification works', function () {
|
|||
});
|
||||
|
||||
test('query parameter identification works', function () {
|
||||
InitializeTenancyByRequestData::$header = null;
|
||||
InitializeTenancyByRequestData::$queryParameter = 'tenant';
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules;
|
|||
use Stancl\Tenancy\Tests\Etc\Tenant as TestTenant;
|
||||
|
||||
beforeEach(function () {
|
||||
BelongsToTenant::$tenantIdColumn = 'tenant_id';
|
||||
|
||||
Schema::create('posts', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('text');
|
||||
|
|
@ -61,7 +59,7 @@ test('secondary models are not scoped to the current tenant when accessed direct
|
|||
expect(Comment::count())->toBe(2);
|
||||
});
|
||||
|
||||
test('secondary models a r e scoped to the current tenant when accessed directly and parent relationship traitis used', function () {
|
||||
test('secondary models ARE scoped to the current tenant when accessed directly and parent relationship trait is used', function () {
|
||||
$acme = Tenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
|
|
@ -144,7 +142,7 @@ test('tenant id is not auto added when creating primary resources in central con
|
|||
});
|
||||
|
||||
test('tenant id column name can be customized', function () {
|
||||
BelongsToTenant::$tenantIdColumn = 'team_id';
|
||||
config(['tenancy.single_db.tenant_id_column' => 'team_id']);
|
||||
|
||||
Schema::drop('comments');
|
||||
Schema::drop('posts');
|
||||
|
|
@ -207,13 +205,13 @@ test('the model returned by the tenant helper has unique and exists validation r
|
|||
$uniqueFails = Validator::make($data, [
|
||||
'slug' => 'unique:posts',
|
||||
])->fails();
|
||||
$existsFails = Validator::make($data, [
|
||||
$existsPass = Validator::make($data, [
|
||||
'slug' => 'exists:posts',
|
||||
])->fails();
|
||||
])->passes();
|
||||
|
||||
// Assert that 'unique' and 'exists' aren't scoped by default
|
||||
// pest()->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists'
|
||||
// pest()->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists'
|
||||
expect($uniqueFails)->toBeTrue(); // Expect unique rule failed to pass because slug 'foo' already exists
|
||||
expect($existsPass)->toBeTrue(); // Expect exists rule pass because slug 'foo' exists
|
||||
|
||||
$uniqueFails = Validator::make($data, [
|
||||
'slug' => tenant()->unique('posts'),
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ test('tenant can be identified by subdomain', function () {
|
|||
|
||||
test('onfail logic can be customized', function () {
|
||||
InitializeTenancyBySubdomain::$onFail = function () {
|
||||
return 'foo';
|
||||
return response('foo');
|
||||
};
|
||||
|
||||
pest()
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@ use Illuminate\Support\Facades\Event;
|
|||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Controllers\TenantAssetsController;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
|
|
@ -21,13 +19,8 @@ beforeEach(function () {
|
|||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Cleanup
|
||||
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class;
|
||||
});
|
||||
|
||||
test('asset can be accessed using the url returned by the tenant asset helper', function () {
|
||||
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
|
||||
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
tenancy()->initialize($tenant);
|
||||
|
|
@ -95,7 +88,7 @@ test('asset helper tenancy can be disabled', function () {
|
|||
});
|
||||
|
||||
test('test asset controller returns a 404 when no path is provided', function () {
|
||||
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
|
||||
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue