mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 19:14:03 +00:00
Merge branch 'master' into phpstan-ci
This commit is contained in:
commit
c0c7855135
144 changed files with 3627 additions and 758 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -11,7 +11,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: abrardev/tenancy:latest
|
container: archtechx/tenancy:latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
.env
|
.env
|
||||||
|
.DS_Store
|
||||||
composer.lock
|
composer.lock
|
||||||
vendor/
|
vendor/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ $rules = [
|
||||||
'operators' => [
|
'operators' => [
|
||||||
'=>' => null,
|
'=>' => null,
|
||||||
'|' => 'no_space',
|
'|' => 'no_space',
|
||||||
|
'&' => 'no_space',
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'blank_line_after_namespace' => true,
|
'blank_line_after_namespace' => true,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
Jobs\CreateDatabase::class,
|
Jobs\CreateDatabase::class,
|
||||||
Jobs\MigrateDatabase::class,
|
Jobs\MigrateDatabase::class,
|
||||||
// Jobs\SeedDatabase::class,
|
// Jobs\SeedDatabase::class,
|
||||||
|
Jobs\CreateStorageSymlinks::class,
|
||||||
|
|
||||||
// Your own jobs to prepare the tenant.
|
// Your own jobs to prepare the tenant.
|
||||||
// Provision API keys, create S3 buckets, anything you want!
|
// Provision API keys, create S3 buckets, anything you want!
|
||||||
|
|
@ -46,14 +47,25 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
])->send(function (Events\DeletingTenant $event) {
|
])->send(function (Events\DeletingTenant $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->shouldBeQueued(false),
|
})->shouldBeQueued(false),
|
||||||
|
|
||||||
|
// Listeners\DeleteTenantStorage::class,
|
||||||
],
|
],
|
||||||
Events\TenantDeleted::class => [
|
Events\TenantDeleted::class => [
|
||||||
JobPipeline::make([
|
JobPipeline::make([
|
||||||
Jobs\DeleteDatabase::class,
|
Jobs\DeleteDatabase::class,
|
||||||
|
Jobs\RemoveStorageSymlinks::class,
|
||||||
])->send(function (Events\TenantDeleted $event) {
|
])->send(function (Events\TenantDeleted $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
||||||
],
|
],
|
||||||
|
Events\TenantMaintenanceModeEnabled::class => [],
|
||||||
|
Events\TenantMaintenanceModeDisabled::class => [],
|
||||||
|
|
||||||
|
// Pending tenant events
|
||||||
|
Events\CreatingPendingTenant::class => [],
|
||||||
|
Events\PendingTenantCreated::class => [],
|
||||||
|
Events\PullingPendingTenant::class => [],
|
||||||
|
Events\PendingTenantPulled::class => [],
|
||||||
|
|
||||||
// Domain events
|
// Domain events
|
||||||
Events\CreatingDomain::class => [],
|
Events\CreatingDomain::class => [],
|
||||||
|
|
@ -93,6 +105,12 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
Listeners\UpdateSyncedResource::class,
|
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)
|
// Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
|
||||||
Events\SyncedResourceChangedInForeignDatabase::class => [],
|
Events\SyncedResourceChangedInForeignDatabase::class => [],
|
||||||
];
|
];
|
||||||
|
|
@ -134,16 +152,8 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
|
|
||||||
protected function makeTenancyMiddlewareHighestPriority()
|
protected function makeTenancyMiddlewareHighestPriority()
|
||||||
{
|
{
|
||||||
$tenancyMiddleware = [
|
// PreventAccessFromCentralDomains has even higher priority than the identification middleware
|
||||||
// Even higher priority than the initialization middleware
|
$tenancyMiddleware = array_merge([Middleware\PreventAccessFromCentralDomains::class], config('tenancy.identification.middleware'));
|
||||||
Middleware\PreventAccessFromCentralDomains::class,
|
|
||||||
|
|
||||||
Middleware\InitializeTenancyByDomain::class,
|
|
||||||
Middleware\InitializeTenancyBySubdomain::class,
|
|
||||||
Middleware\InitializeTenancyByDomainOrSubdomain::class,
|
|
||||||
Middleware\InitializeTenancyByPath::class,
|
|
||||||
Middleware\InitializeTenancyByRequestData::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach (array_reverse($tenancyMiddleware) as $middleware) {
|
foreach (array_reverse($tenancyMiddleware) as $middleware) {
|
||||||
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
|
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Stancl\Tenancy\Database\Models\Domain;
|
use Stancl\Tenancy\Middleware;
|
||||||
use Stancl\Tenancy\Database\Models\Tenant;
|
use Stancl\Tenancy\Resolvers;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tenant_model' => Tenant::class,
|
'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class,
|
||||||
'id_generator' => Stancl\Tenancy\UUIDGenerator::class,
|
'domain_model' => Stancl\Tenancy\Database\Models\Domain::class,
|
||||||
|
|
||||||
'domain_model' => Domain::class,
|
'id_generator' => Stancl\Tenancy\UUIDGenerator::class,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of domains hosting your central app.
|
* The list of domains hosting your central app.
|
||||||
|
|
@ -21,6 +21,56 @@ return [
|
||||||
'localhost',
|
'localhost',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'identification' => [
|
||||||
|
/**
|
||||||
|
* The default middleware used for tenant identification.
|
||||||
|
*
|
||||||
|
* If you use multiple forms of identification, you can set this to the "main" approach you use.
|
||||||
|
*/
|
||||||
|
'default_middleware' => Middleware\InitializeTenancyByDomain::class,// todo@identification add this to a 'tenancy' mw group
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All of the identification middleware used by the package.
|
||||||
|
*
|
||||||
|
* If you write your own, make sure to add them to this array.
|
||||||
|
*/
|
||||||
|
'middleware' => [
|
||||||
|
Middleware\InitializeTenancyByDomain::class,
|
||||||
|
Middleware\InitializeTenancyBySubdomain::class,
|
||||||
|
Middleware\InitializeTenancyByDomainOrSubdomain::class,
|
||||||
|
Middleware\InitializeTenancyByPath::class,
|
||||||
|
Middleware\InitializeTenancyByRequestData::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tenant resolvers used by the package.
|
||||||
|
*
|
||||||
|
* Resolvers which implement the CachedTenantResolver contract have options for configuring the caching details.
|
||||||
|
* If you add your own resolvers, do not add the 'cache' key unless your resolver is based on CachedTenantResolver.
|
||||||
|
*/
|
||||||
|
'resolvers' => [
|
||||||
|
Resolvers\DomainTenantResolver::class => [
|
||||||
|
'cache' => false,
|
||||||
|
'cache_ttl' => 3600, // seconds
|
||||||
|
'cache_store' => null, // default
|
||||||
|
],
|
||||||
|
Resolvers\PathTenantResolver::class => [
|
||||||
|
'tenant_parameter_name' => 'tenant',
|
||||||
|
|
||||||
|
'cache' => false,
|
||||||
|
'cache_ttl' => 3600, // seconds
|
||||||
|
'cache_store' => null, // default
|
||||||
|
],
|
||||||
|
Resolvers\RequestDataTenantResolver::class => [
|
||||||
|
'cache' => false,
|
||||||
|
'cache_ttl' => 3600, // seconds
|
||||||
|
'cache_store' => null, // default
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
// todo@docs update integration guides to use Stancl\Tenancy::defaultMiddleware()
|
||||||
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenancy bootstrappers are executed when tenancy is initialized.
|
* Tenancy bootstrappers are executed when tenancy is initialized.
|
||||||
* Their responsibility is making Laravel features tenant-aware.
|
* Their responsibility is making Laravel features tenant-aware.
|
||||||
|
|
@ -32,9 +82,29 @@ return [
|
||||||
Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
|
Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
|
||||||
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
|
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
|
||||||
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
|
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
|
||||||
|
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
|
||||||
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending tenants config.
|
||||||
|
* This is useful if you're looking for a way to always have a tenant ready to be used.
|
||||||
|
*/
|
||||||
|
'pending' => [
|
||||||
|
/**
|
||||||
|
* If disabled, pending tenants will be excluded from all tenant queries.
|
||||||
|
* You can still use ::withPending(), ::withoutPending() and ::onlyPending() to include or exclude the pending tenants regardless of this setting.
|
||||||
|
* Note: when disabled, this will also ignore pending tenants when running the tenant commands (migration, seed, etc.)
|
||||||
|
*/
|
||||||
|
'include_in_queries' => true,
|
||||||
|
/**
|
||||||
|
* Defines how many pending tenants you want to have ready in the pending tenant pool.
|
||||||
|
* This depends on the volume of tenants you're creating.
|
||||||
|
*/
|
||||||
|
'count' => env('TENANCY_PENDING_COUNT', 5),
|
||||||
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database tenancy config. Used by DatabaseTenancyBootstrapper.
|
* Database tenancy config. Used by DatabaseTenancyBootstrapper.
|
||||||
*/
|
*/
|
||||||
|
|
@ -47,6 +117,11 @@ return [
|
||||||
*/
|
*/
|
||||||
'template_tenant_connection' => null,
|
'template_tenant_connection' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the temporary connection used for creating and deleting tenant databases.
|
||||||
|
*/
|
||||||
|
'tenant_host_connection_name' => 'tenant_host_connection',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenant database names are created like this:
|
* Tenant database names are created like this:
|
||||||
* prefix + tenant_id + suffix.
|
* prefix + tenant_id + suffix.
|
||||||
|
|
@ -75,6 +150,9 @@ return [
|
||||||
*/
|
*/
|
||||||
// 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database
|
// 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// todo docblock
|
||||||
|
'drop_tenant_databases_on_migrate_fresh' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -118,6 +196,24 @@ return [
|
||||||
'public' => '%storage_path%/app/public/',
|
'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.
|
* Should storage_path() be suffixed.
|
||||||
*
|
*
|
||||||
|
|
@ -186,6 +282,7 @@ return [
|
||||||
'migration_parameters' => [
|
'migration_parameters' => [
|
||||||
'--force' => true, // This needs to be true to run migrations in production.
|
'--force' => true, // This needs to be true to run migrations in production.
|
||||||
'--path' => [database_path('migrations/tenant')],
|
'--path' => [database_path('migrations/tenant')],
|
||||||
|
'--schema-path' => database_path('schema/tenant-schema.dump'),
|
||||||
'--realpath' => true,
|
'--realpath' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -193,7 +290,15 @@ return [
|
||||||
* Parameters used by the tenants:seed command.
|
* Parameters used by the tenants:seed command.
|
||||||
*/
|
*/
|
||||||
'seeder_parameters' => [
|
'seeder_parameters' => [
|
||||||
'--class' => 'DatabaseSeeder', // root seeder class
|
'--class' => 'Database\Seeders\DatabaseSeeder', // root seeder class
|
||||||
// '--force' => true,
|
// '--force' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single-database tenancy config.
|
||||||
|
*/
|
||||||
|
'single_db' => [
|
||||||
|
/** The name of the column used by models with the BelongsToTenant trait. */
|
||||||
|
'tenant_id_column' => 'tenant_id',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Stancl\Tenancy\Controllers\TenantAssetController;
|
||||||
|
|
||||||
Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset')
|
Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset'])
|
||||||
->where('path', '(.*)')
|
->where('path', '(.*)')
|
||||||
->name('stancl.tenancy.asset');
|
->name('stancl.tenancy.asset');
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,10 @@
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"illuminate/support": "^9.0",
|
"illuminate/support": "^9.0",
|
||||||
"facade/ignition-contracts": "^1.0",
|
"spatie/ignition": "^1.4",
|
||||||
"ramsey/uuid": "^4.0",
|
"ramsey/uuid": "^4.0",
|
||||||
"stancl/jobpipeline": "^1.0",
|
"stancl/jobpipeline": "^1.0",
|
||||||
"stancl/virtualcolumn": "^1.0"
|
"stancl/virtualcolumn": "^1.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/framework": "^9.0",
|
"laravel/framework": "^9.0",
|
||||||
|
|
@ -30,7 +30,8 @@
|
||||||
"doctrine/dbal": "^2.10",
|
"doctrine/dbal": "^2.10",
|
||||||
"spatie/valuestore": "^1.2.5",
|
"spatie/valuestore": "^1.2.5",
|
||||||
"pestphp/pest": "^1.21",
|
"pestphp/pest": "^1.21",
|
||||||
"nunomaduro/larastan": "^1.0"
|
"nunomaduro/larastan": "^1.0",
|
||||||
|
"spatie/invade": "^1.1"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
@ -62,7 +63,8 @@
|
||||||
"docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build",
|
"docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build",
|
||||||
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
|
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
|
||||||
"coverage": "open coverage/phpunit/html/index.html",
|
"coverage": "open coverage/phpunit/html/index.html",
|
||||||
"phpstan": "vendor/bin/phpstan",
|
"phpstan": "vendor/bin/phpstan --pro",
|
||||||
|
"cs": "php-cs-fixer fix --config=.php-cs-fixer.php",
|
||||||
"test": "PHP_VERSION=8.1 ./test --no-coverage",
|
"test": "PHP_VERSION=8.1 ./test --no-coverage",
|
||||||
"test-full": "PHP_VERSION=8.1 ./test"
|
"test-full": "PHP_VERSION=8.1 ./test"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ services:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
# mssql:
|
||||||
|
# condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- .:/var/www/html:delegated
|
- .:/var/www/html:delegated
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -74,4 +76,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- ACCEPT_EULA=Y
|
- ACCEPT_EULA=Y
|
||||||
- SA_PASSWORD=P@ssword # todo reuse values from env above
|
- SA_PASSWORD=P@ssword # todo reuse values from env above
|
||||||
# todo missing health check
|
healthcheck:
|
||||||
|
test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P P@ssword -Q "SELECT 1" -b -o /dev/null
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
|
|
||||||
32
phpstan.neon
32
phpstan.neon
|
|
@ -1,5 +1,6 @@
|
||||||
includes:
|
includes:
|
||||||
- ./vendor/nunomaduro/larastan/extension.neon
|
- ./vendor/nunomaduro/larastan/extension.neon
|
||||||
|
- ./vendor/spatie/invade/phpstan-extension.neon
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
paths:
|
paths:
|
||||||
|
|
@ -10,16 +11,39 @@ parameters:
|
||||||
|
|
||||||
universalObjectCratesClasses:
|
universalObjectCratesClasses:
|
||||||
- Illuminate\Routing\Route
|
- Illuminate\Routing\Route
|
||||||
|
- Illuminate\Database\Eloquent\Model
|
||||||
|
|
||||||
ignoreErrors:
|
ignoreErrors:
|
||||||
-
|
- '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#'
|
||||||
message: '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#'
|
- '#Cannot access offset (.*?) on Illuminate\\Contracts\\Config\\Repository#'
|
||||||
paths:
|
|
||||||
- src/TenancyServiceProvider.php
|
|
||||||
-
|
-
|
||||||
message: '#invalid type Laravel\\Telescope\\IncomingEntry#'
|
message: '#invalid type Laravel\\Telescope\\IncomingEntry#'
|
||||||
paths:
|
paths:
|
||||||
- src/Features/TelescopeTags.php
|
- src/Features/TelescopeTags.php
|
||||||
|
-
|
||||||
|
message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getRelationshipToPrimaryModel\(\)#'
|
||||||
|
paths:
|
||||||
|
- src/Database/ParentModelScope.php
|
||||||
|
-
|
||||||
|
message: '#Parameter \#1 \$key of method Illuminate\\Contracts\\Cache\\Repository::put\(\) expects string#'
|
||||||
|
paths:
|
||||||
|
- src/helpers.php
|
||||||
|
-
|
||||||
|
message: '#PHPDoc tag \@param has invalid value \(dynamic#'
|
||||||
|
paths:
|
||||||
|
- src/helpers.php
|
||||||
|
-
|
||||||
|
message: '#Illuminate\\Routing\\UrlGenerator#'
|
||||||
|
paths:
|
||||||
|
- src/Bootstrappers/FilesystemTenancyBootstrapper.php
|
||||||
|
-
|
||||||
|
message: '#select\(\) expects string, Illuminate\\Database\\Query\\Expression given#'
|
||||||
|
paths:
|
||||||
|
- src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php
|
||||||
|
-
|
||||||
|
message: '#Trying to invoke Closure\|null but it might not be a callable#'
|
||||||
|
paths:
|
||||||
|
- src/Database/DatabaseConfig.php
|
||||||
|
|
||||||
checkMissingIterableValueType: false
|
checkMissingIterableValueType: false
|
||||||
treatPhpDocTypesAsCertain: false
|
treatPhpDocTypesAsCertain: false
|
||||||
|
|
|
||||||
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
|
class CacheTenancyBootstrapper implements TenancyBootstrapper
|
||||||
{
|
{
|
||||||
/** @var CacheManager */
|
protected ?CacheManager $originalCache = null;
|
||||||
protected $originalCache;
|
|
||||||
|
|
||||||
/** @var Application */
|
public function __construct(
|
||||||
protected $app;
|
protected Application $app
|
||||||
|
) {
|
||||||
public function __construct(Application $app)
|
|
||||||
{
|
|
||||||
$this->app = $app;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant)
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$this->resetFacadeCache();
|
$this->resetFacadeCache();
|
||||||
|
|
||||||
|
|
@ -34,7 +30,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revert()
|
public function revert(): void
|
||||||
{
|
{
|
||||||
$this->resetFacadeCache();
|
$this->resetFacadeCache();
|
||||||
|
|
||||||
|
|
@ -50,7 +46,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
|
||||||
* facade has been made prior to bootstrapping tenancy. The
|
* facade has been made prior to bootstrapping tenancy. The
|
||||||
* facade has its own cache, separate from the container.
|
* facade has its own cache, separate from the container.
|
||||||
*/
|
*/
|
||||||
public function resetFacadeCache()
|
public function resetFacadeCache(): void
|
||||||
{
|
{
|
||||||
Cache::clearResolvedInstances();
|
Cache::clearResolvedInstances();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
use Stancl\Tenancy\Database\DatabaseManager;
|
use Stancl\Tenancy\Database\DatabaseManager;
|
||||||
use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException;
|
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||||
|
|
||||||
class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
||||||
{
|
{
|
||||||
|
|
@ -20,7 +20,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant)
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
/** @var TenantWithDatabase $tenant */
|
/** @var TenantWithDatabase $tenant */
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$this->database->connectToTenant($tenant);
|
$this->database->connectToTenant($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revert()
|
public function revert(): void
|
||||||
{
|
{
|
||||||
$this->database->reconnectToCentral();
|
$this->database->reconnectToCentral();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Bootstrappers;
|
namespace Stancl\Tenancy\Bootstrappers;
|
||||||
|
|
||||||
use Illuminate\Contracts\Foundation\Application;
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
|
use Illuminate\Routing\UrlGenerator;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
@ -27,13 +28,14 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->app['url']->macro('setAssetRoot', function ($root) {
|
$this->app['url']->macro('setAssetRoot', function ($root) {
|
||||||
|
/** @var UrlGenerator $this */
|
||||||
$this->assetRoot = $root;
|
$this->assetRoot = $root;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant)
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->getTenantKey();
|
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->getTenantKey();
|
||||||
|
|
||||||
|
|
@ -45,7 +47,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
// asset()
|
// asset()
|
||||||
if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy'] ?? true) {
|
if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy'] ?? true) {
|
||||||
if ($this->originalPaths['asset_url']) {
|
if ($this->originalPaths['asset_url']) {
|
||||||
$this->app['config']['app.asset_url'] = ($this->originalPaths['asset_url'] ?? $this->app['config']['app.url']) . "/$suffix";
|
$this->app['config']['app.asset_url'] = $this->originalPaths['asset_url'] . "/$suffix";
|
||||||
$this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
|
$this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
|
||||||
} else {
|
} else {
|
||||||
$this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => '']));
|
$this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => '']));
|
||||||
|
|
@ -57,9 +59,10 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
|
||||||
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
||||||
// todo@v4 \League\Flysystem\PathPrefixer is making this a lot more painful in flysystem v2
|
// 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']['path'][$disk] = $originalRoot;
|
||||||
$this->originalPaths['disks'][$disk] = $originalRoot;
|
|
||||||
|
|
||||||
$finalPrefix = str_replace(
|
$finalPrefix = str_replace(
|
||||||
['%storage_path%', '%tenant%'],
|
['%storage_path%', '%tenant%'],
|
||||||
|
|
@ -74,10 +77,23 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix;
|
$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()
|
// storage_path()
|
||||||
$this->app->useStoragePath($this->originalPaths['storage']);
|
$this->app->useStoragePath($this->originalPaths['storage']);
|
||||||
|
|
@ -88,8 +104,16 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
|
||||||
// Storage facade
|
// Storage facade
|
||||||
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
|
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
|
||||||
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
foreach ($this->app['config']['tenancy.filesystem.disks'] as $diskName) {
|
||||||
$this->app['config']["filesystems.disks.{$disk}.root"] = $this->originalPaths['disks'][$disk];
|
$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php
Normal file
38
src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Bootstrappers\Integrations;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Config\Repository;
|
||||||
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
class ScoutTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
{
|
||||||
|
protected ?string $originalScoutPrefix = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected Repository $config,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bootstrap(Tenant $tenant): void
|
||||||
|
{
|
||||||
|
if ($this->originalScoutPrefix !== null) {
|
||||||
|
$this->originalScoutPrefix = $this->config->get('scout.prefix');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->config->set('scout.prefix', $this->getTenantPrefix($tenant));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function revert(): void
|
||||||
|
{
|
||||||
|
$this->config->set('scout.prefix', $this->originalScoutPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTenantPrefix(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
return (string) $tenant->getTenantKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
* However, we're registering a hook to initialize tenancy. Therefore,
|
* However, we're registering a hook to initialize tenancy. Therefore,
|
||||||
* we need to register the hook at service provider execution time.
|
* we need to register the hook at service provider execution time.
|
||||||
*/
|
*/
|
||||||
public static function __constructStatic(Application $app)
|
public static function __constructStatic(Application $app): void
|
||||||
{
|
{
|
||||||
static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests());
|
static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests());
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +52,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$this->setUpPayloadGenerator();
|
$this->setUpPayloadGenerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function setUpJobListener($dispatcher, $runningTests)
|
protected static function setUpJobListener(Dispatcher $dispatcher, bool $runningTests): void
|
||||||
{
|
{
|
||||||
$previousTenant = null;
|
$previousTenant = null;
|
||||||
|
|
||||||
|
|
@ -62,14 +62,11 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
|
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (version_compare(app()->version(), '8.64', '>=')) {
|
|
||||||
// JobRetryRequested only exists since Laravel 8.64
|
|
||||||
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
|
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
|
||||||
$previousTenant = tenant();
|
$previousTenant = tenant();
|
||||||
|
|
||||||
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
|
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
|
||||||
$revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) {
|
$revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) {
|
||||||
|
|
@ -82,7 +79,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
|
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function initializeTenancyForQueue($tenantId)
|
protected static function initializeTenancyForQueue(string|int $tenantId): void
|
||||||
{
|
{
|
||||||
if (! $tenantId) {
|
if (! $tenantId) {
|
||||||
// The job is not tenant-aware
|
// The job is not tenant-aware
|
||||||
|
|
@ -100,7 +97,9 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
}
|
}
|
||||||
|
|
||||||
tenancy()->initialize(tenancy()->find($tenantId));
|
/** @var Tenant $tenant */
|
||||||
|
$tenant = tenancy()->find($tenantId);
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -115,10 +114,13 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
|
||||||
// Tenancy was either not initialized, or initialized for a different tenant.
|
// Tenancy was either not initialized, or initialized for a different tenant.
|
||||||
// Therefore, we initialize it for the correct tenant.
|
// Therefore, we initialize it for the correct tenant.
|
||||||
tenancy()->initialize(tenancy()->find($tenantId));
|
|
||||||
|
/** @var Tenant $tenant */
|
||||||
|
$tenant = tenancy()->find($tenantId);
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function revertToPreviousState($event, &$previousTenant)
|
protected static function revertToPreviousState(JobProcessed|JobFailed $event, ?Tenant &$previousTenant): void
|
||||||
{
|
{
|
||||||
$tenantId = $event->job->payload()['tenant_id'] ?? null;
|
$tenantId = $event->job->payload()['tenant_id'] ?? null;
|
||||||
|
|
||||||
|
|
@ -138,7 +140,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setUpPayloadGenerator()
|
protected function setUpPayloadGenerator(): void
|
||||||
{
|
{
|
||||||
$bootstrapper = &$this;
|
$bootstrapper = &$this;
|
||||||
|
|
||||||
|
|
@ -149,17 +151,17 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant)
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revert()
|
public function revert(): void
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPayload(string $connection)
|
public function getPayload(string $connection): array
|
||||||
{
|
{
|
||||||
if (! tenancy()->initialized) {
|
if (! tenancy()->initialized) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,21 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant)
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
foreach ($this->prefixedConnections() as $connection) {
|
foreach ($this->prefixedConnections() as $connection) {
|
||||||
$prefix = $this->config['tenancy.redis.prefix_base'] . $tenant->getTenantKey();
|
$prefix = $this->config['tenancy.redis.prefix_base'] . $tenant->getTenantKey();
|
||||||
$client = Redis::connection($connection)->client();
|
$client = Redis::connection($connection)->client();
|
||||||
|
|
||||||
$this->originalPrefixes[$connection] = $client->getOption($client::OPT_PREFIX);
|
/** @var string $originalPrefix */
|
||||||
|
$originalPrefix = $client->getOption($client::OPT_PREFIX);
|
||||||
|
|
||||||
|
$this->originalPrefixes[$connection] = $originalPrefix;
|
||||||
$client->setOption($client::OPT_PREFIX, $prefix);
|
$client->setOption($client::OPT_PREFIX, $prefix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revert()
|
public function revert(): void
|
||||||
{
|
{
|
||||||
foreach ($this->prefixedConnections() as $connection) {
|
foreach ($this->prefixedConnections() as $connection) {
|
||||||
$client = Redis::connection($connection)->client();
|
$client = Redis::connection($connection)->client();
|
||||||
|
|
@ -44,7 +47,8 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$this->originalPrefixes = [];
|
$this->originalPrefixes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function prefixedConnections()
|
/** @return string[] */
|
||||||
|
protected function prefixedConnections(): array
|
||||||
{
|
{
|
||||||
return $this->config['tenancy.redis.prefixed_connections'];
|
return $this->config['tenancy.redis.prefixed_connections'];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
74
src/Commands/ClearPendingTenants.php
Normal file
74
src/Commands/ClearPendingTenants.php
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class ClearPendingTenants extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'tenants:pending-clear
|
||||||
|
{--all : Override the default settings and deletes all pending tenants}
|
||||||
|
{--older-than-days= : Deletes all pending tenants older than the amount of days}
|
||||||
|
{--older-than-hours= : Deletes all pending tenants older than the amount of hours}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Remove pending tenants.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Removing pending tenants.');
|
||||||
|
|
||||||
|
$expirationDate = now();
|
||||||
|
// We compare the original expiration date to the new one to check if the new one is different later
|
||||||
|
$originalExpirationDate = $expirationDate->copy()->toImmutable();
|
||||||
|
|
||||||
|
// Skip the time constraints if the 'all' option is given
|
||||||
|
if (! $this->option('all')) {
|
||||||
|
$olderThanDays = $this->option('older-than-days');
|
||||||
|
$olderThanHours = $this->option('older-than-hours');
|
||||||
|
|
||||||
|
if ($olderThanDays && $olderThanHours) {
|
||||||
|
$this->line("<options=bold,reverse;fg=red> Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components
|
||||||
|
$this->line('Please, choose only one of these options.');
|
||||||
|
|
||||||
|
return 1; // Exit code for failure
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($olderThanDays) {
|
||||||
|
$expirationDate->subDays($olderThanDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($olderThanHours) {
|
||||||
|
$expirationDate->subHours($olderThanHours);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$deletedTenantCount = tenancy()
|
||||||
|
->query()
|
||||||
|
->onlyPending()
|
||||||
|
->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) {
|
||||||
|
$query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp);
|
||||||
|
})
|
||||||
|
->get()
|
||||||
|
->each // Trigger the model events by deleting the tenants one by one
|
||||||
|
->delete()
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.');
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/Commands/CreatePendingTenants.php
Normal file
62
src/Commands/CreatePendingTenants.php
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CreatePendingTenants extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'tenants:pending-create {--count= : The number of pending tenants to be created}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Create pending tenants.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Creating pending tenants.');
|
||||||
|
|
||||||
|
$maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count'));
|
||||||
|
$pendingTenantCount = $this->getPendingTenantCount();
|
||||||
|
$createdCount = 0;
|
||||||
|
|
||||||
|
while ($pendingTenantCount < $maxPendingTenantCount) {
|
||||||
|
tenancy()->model()::createPending();
|
||||||
|
|
||||||
|
// Fetching the pending tenant count in each iteration prevents creating too many tenants
|
||||||
|
// If pending tenants are being created somewhere else while running this command
|
||||||
|
$pendingTenantCount = $this->getPendingTenantCount();
|
||||||
|
|
||||||
|
$createdCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.');
|
||||||
|
$this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the number of currently available pending tenants.
|
||||||
|
*/
|
||||||
|
private function getPendingTenantCount(): int
|
||||||
|
{
|
||||||
|
return tenancy()
|
||||||
|
->query()
|
||||||
|
->onlyPending()
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Commands/Down.php
Normal file
53
src/Commands/Down.php
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Console\DownCommand;
|
||||||
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
|
|
||||||
|
class Down extends DownCommand
|
||||||
|
{
|
||||||
|
use HasTenantOptions;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$payload = $this->getDownDatabasePayload();
|
||||||
|
|
||||||
|
tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($payload) {
|
||||||
|
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||||
|
$tenant->putDownForMaintenance($payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->components->info('Tenants are now in maintenance mode.');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the payload to be placed in the "down" file. This
|
||||||
|
* payload is the same as the original function
|
||||||
|
* but without the 'template' option.
|
||||||
|
*/
|
||||||
|
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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,63 +4,136 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Commands;
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class Install extends Command
|
class Install extends Command
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'tenancy:install';
|
protected $signature = 'tenancy:install';
|
||||||
|
|
||||||
/**
|
protected $description = 'Install Tenancy for Laravel.';
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Install stancl/tenancy.';
|
|
||||||
|
|
||||||
/**
|
public function handle(): int
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
{
|
||||||
$this->comment('Installing stancl/tenancy...');
|
$this->step(
|
||||||
$this->callSilent('vendor:publish', [
|
name: 'Publishing config file',
|
||||||
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
|
tag: 'config',
|
||||||
'--tag' => 'config',
|
file: 'config/tenancy.php',
|
||||||
]);
|
newLineBefore: true,
|
||||||
$this->info('✔️ Created config/tenancy.php');
|
);
|
||||||
|
|
||||||
if (! file_exists(base_path('routes/tenant.php'))) {
|
$this->step(
|
||||||
$this->callSilent('vendor:publish', [
|
name: 'Publishing routes',
|
||||||
|
tag: 'routes',
|
||||||
|
file: 'routes/tenant.php',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->step(
|
||||||
|
name: 'Publishing service provider',
|
||||||
|
tag: 'providers',
|
||||||
|
file: 'app/Providers/TenancyServiceProvider.php',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->step(
|
||||||
|
name: 'Publishing migrations',
|
||||||
|
tag: 'migrations',
|
||||||
|
files: [
|
||||||
|
'database/migrations/2019_09_15_000010_create_tenants_table.php',
|
||||||
|
'database/migrations/2019_09_15_000020_create_domains_table.php',
|
||||||
|
],
|
||||||
|
warning: 'Migrations already exist',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->step(
|
||||||
|
name: 'Creating [database/migrations/tenant] folder',
|
||||||
|
task: fn () => mkdir(database_path('migrations/tenant')),
|
||||||
|
unless: is_dir(database_path('migrations/tenant')),
|
||||||
|
warning: 'Folder [database/migrations/tenant] already exists.',
|
||||||
|
newLineAfter: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->components->info('✨️ Tenancy for Laravel successfully installed.');
|
||||||
|
|
||||||
|
$this->askForSupport();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a step of the installation process.
|
||||||
|
*
|
||||||
|
* @param string $name The name of the step.
|
||||||
|
* @param Closure|null $task The task code.
|
||||||
|
* @param bool $unless Condition specifying when the task should NOT run.
|
||||||
|
* @param string|null $warning Warning shown when the $unless condition is true.
|
||||||
|
* @param string|null $file Name of the file being added.
|
||||||
|
* @param string|null $tag The tag being published.
|
||||||
|
* @param array|null $files Names of files being added.
|
||||||
|
* @param bool $newLineBefore Should a new line be printed after the step.
|
||||||
|
* @param bool $newLineAfter Should a new line be printed after the step.
|
||||||
|
*/
|
||||||
|
protected function step(
|
||||||
|
string $name,
|
||||||
|
Closure $task = null,
|
||||||
|
bool $unless = false,
|
||||||
|
string $warning = null,
|
||||||
|
string $file = null,
|
||||||
|
string $tag = null,
|
||||||
|
array $files = null,
|
||||||
|
bool $newLineBefore = false,
|
||||||
|
bool $newLineAfter = false,
|
||||||
|
): void {
|
||||||
|
if ($file) {
|
||||||
|
$name .= " [$file]"; // Append clickable path to the task name
|
||||||
|
$unless = file_exists(base_path($file)); // Make the condition a check for the file's existence
|
||||||
|
$warning = "File [$file] already exists."; // Make the warning a message about the file already existing
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tag) {
|
||||||
|
$task = fn () => $this->callSilent('vendor:publish', [
|
||||||
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
|
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
|
||||||
'--tag' => 'routes',
|
'--tag' => $tag,
|
||||||
]);
|
]);
|
||||||
$this->info('✔️ Created routes/tenant.php');
|
}
|
||||||
|
|
||||||
|
if ($files) {
|
||||||
|
// Show a warning if any of the files already exist
|
||||||
|
$unless = count(array_filter($files, fn ($file) => file_exists(base_path($file)))) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $unless) {
|
||||||
|
if ($newLineBefore) {
|
||||||
|
$this->newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->components->task($name, $task ?? fn () => null);
|
||||||
|
|
||||||
|
if ($files) {
|
||||||
|
// Print out a clickable list of the added files
|
||||||
|
$this->components->bulletList(array_map(fn (string $file) => "[$file]", $files));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newLineAfter) {
|
||||||
|
$this->newLine();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->info('Found routes/tenant.php.');
|
$this->components->warn($warning);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$this->callSilent('vendor:publish', [
|
|
||||||
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
|
/** If the user accepts, opens the GitHub project in the browser. */
|
||||||
'--tag' => 'providers',
|
public function askForSupport(): void
|
||||||
]);
|
{
|
||||||
$this->info('✔️ Created TenancyServiceProvider.php');
|
if ($this->components->confirm('Would you like to show your support by starring the project on GitHub?', true)) {
|
||||||
|
if (PHP_OS_FAMILY === 'Darwin') {
|
||||||
$this->callSilent('vendor:publish', [
|
exec('open https://github.com/archtechx/tenancy');
|
||||||
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
|
}
|
||||||
'--tag' => 'migrations',
|
if (PHP_OS_FAMILY === 'Windows') {
|
||||||
]);
|
exec('start https://github.com/archtechx/tenancy');
|
||||||
$this->info('✔️ Created migrations. Remember to run [php artisan migrate]!');
|
}
|
||||||
|
if (PHP_OS_FAMILY === 'Linux') {
|
||||||
if (! is_dir(database_path('migrations/tenant'))) {
|
exec('xdg-open https://github.com/archtechx/tenancy');
|
||||||
mkdir(database_path('migrations/tenant'));
|
}
|
||||||
$this->info('✔️ Created database/migrations/tenant folder.');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$this->comment('✨️ stancl/tenancy installed successfully.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
src/Commands/Link.php
Normal file
62
src/Commands/Link.php
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?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\HasTenantOptions;
|
||||||
|
|
||||||
|
class Link extends Command
|
||||||
|
{
|
||||||
|
use HasTenantOptions;
|
||||||
|
|
||||||
|
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(): int
|
||||||
|
{
|
||||||
|
$tenants = $this->getTenants();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($this->option('remove')) {
|
||||||
|
$this->removeLinks($tenants);
|
||||||
|
} else {
|
||||||
|
$this->createLinks($tenants);
|
||||||
|
}
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
$this->error($exception->getMessage());
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function removeLinks(LazyCollection $tenants): void
|
||||||
|
{
|
||||||
|
RemoveStorageSymlinksAction::handle($tenants);
|
||||||
|
|
||||||
|
$this->components->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->components->info('The links have been created.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,13 +9,13 @@ use Illuminate\Database\Console\Migrations\MigrateCommand;
|
||||||
use Illuminate\Database\Migrations\Migrator;
|
use Illuminate\Database\Migrations\Migrator;
|
||||||
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||||
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Stancl\Tenancy\Events\DatabaseMigrated;
|
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||||
use Stancl\Tenancy\Events\MigratingDatabase;
|
use Stancl\Tenancy\Events\MigratingDatabase;
|
||||||
|
|
||||||
class Migrate extends MigrateCommand
|
class Migrate extends MigrateCommand
|
||||||
{
|
{
|
||||||
use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand;
|
use HasTenantOptions, DealsWithMigrations, ExtendsLaravelCommand;
|
||||||
|
|
||||||
protected $description = 'Run migrations for tenant(s)';
|
protected $description = 'Run migrations for tenant(s)';
|
||||||
|
|
||||||
|
|
@ -31,10 +31,7 @@ class Migrate extends MigrateCommand
|
||||||
$this->specifyParameters();
|
$this->specifyParameters();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function handle(): int
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
{
|
||||||
foreach (config('tenancy.migration_parameters') as $parameter => $value) {
|
foreach (config('tenancy.migration_parameters') as $parameter => $value) {
|
||||||
if (! $this->input->hasParameterOption($parameter)) {
|
if (! $this->input->hasParameterOption($parameter)) {
|
||||||
|
|
@ -43,11 +40,11 @@ class Migrate extends MigrateCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->confirmToProceed()) {
|
if (! $this->confirmToProceed()) {
|
||||||
return;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||||
|
|
||||||
event(new MigratingDatabase($tenant));
|
event(new MigratingDatabase($tenant));
|
||||||
|
|
||||||
|
|
@ -56,5 +53,7 @@ class Migrate extends MigrateCommand
|
||||||
|
|
||||||
event(new DatabaseMigrated($tenant));
|
event(new DatabaseMigrated($tenant));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,13 @@ namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
final class MigrateFresh extends Command
|
class MigrateFresh extends Command
|
||||||
{
|
{
|
||||||
use HasATenantsOption, DealsWithMigrations;
|
use HasTenantOptions, DealsWithMigrations;
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Drop all tables and re-run all migrations for tenant(s)';
|
protected $description = 'Drop all tables and re-run all migrations for tenant(s)';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|
@ -29,26 +24,27 @@ final class MigrateFresh extends Command
|
||||||
$this->setName('tenants:migrate-fresh');
|
$this->setName('tenants:migrate-fresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function handle(): int
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
{
|
||||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||||
$this->info('Dropping tables.');
|
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||||
$this->call('db:wipe', array_filter([
|
|
||||||
|
$this->components->task('Dropping tables', function () {
|
||||||
|
$this->callSilently('db:wipe', array_filter([
|
||||||
'--database' => 'tenant',
|
'--database' => 'tenant',
|
||||||
'--drop-views' => $this->option('drop-views'),
|
'--drop-views' => $this->option('drop-views'),
|
||||||
'--force' => true,
|
'--force' => true,
|
||||||
]));
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
$this->info('Migrating.');
|
$this->components->task('Migrating', function () use ($tenant) {
|
||||||
$this->callSilent('tenants:migrate', [
|
$this->callSilent('tenants:migrate', [
|
||||||
'--tenants' => [$tenant->getTenantKey()],
|
'--tenants' => [$tenant->getTenantKey()],
|
||||||
'--force' => true,
|
'--force' => true,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$this->info('Done.');
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
src/Commands/MigrateFreshOverride.php
Normal file
19
src/Commands/MigrateFreshOverride.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||||
|
|
||||||
|
class MigrateFreshOverride extends FreshCommand
|
||||||
|
{
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if (config('tenancy.database.drop_tenant_databases_on_migrate_fresh')) {
|
||||||
|
tenancy()->model()::cursor()->each->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,31 +8,16 @@ use Illuminate\Database\Console\Migrations\RollbackCommand;
|
||||||
use Illuminate\Database\Migrations\Migrator;
|
use Illuminate\Database\Migrations\Migrator;
|
||||||
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||||
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Stancl\Tenancy\Events\DatabaseRolledBack;
|
use Stancl\Tenancy\Events\DatabaseRolledBack;
|
||||||
use Stancl\Tenancy\Events\RollingBackDatabase;
|
use Stancl\Tenancy\Events\RollingBackDatabase;
|
||||||
|
|
||||||
class Rollback extends RollbackCommand
|
class Rollback extends RollbackCommand
|
||||||
{
|
{
|
||||||
use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand;
|
use HasTenantOptions, DealsWithMigrations, ExtendsLaravelCommand;
|
||||||
|
|
||||||
protected static function getTenantCommandName(): string
|
|
||||||
{
|
|
||||||
return 'tenants:rollback';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Rollback migrations for tenant(s).';
|
protected $description = 'Rollback migrations for tenant(s).';
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new command instance.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(Migrator $migrator)
|
public function __construct(Migrator $migrator)
|
||||||
{
|
{
|
||||||
parent::__construct($migrator);
|
parent::__construct($migrator);
|
||||||
|
|
@ -40,10 +25,7 @@ class Rollback extends RollbackCommand
|
||||||
$this->specifyTenantSignature();
|
$this->specifyTenantSignature();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function handle(): int
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
{
|
||||||
foreach (config('tenancy.migration_parameters') as $parameter => $value) {
|
foreach (config('tenancy.migration_parameters') as $parameter => $value) {
|
||||||
if (! $this->input->hasParameterOption($parameter)) {
|
if (! $this->input->hasParameterOption($parameter)) {
|
||||||
|
|
@ -52,11 +34,11 @@ class Rollback extends RollbackCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->confirmToProceed()) {
|
if (! $this->confirmToProceed()) {
|
||||||
return;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||||
|
|
||||||
event(new RollingBackDatabase($tenant));
|
event(new RollingBackDatabase($tenant));
|
||||||
|
|
||||||
|
|
@ -65,5 +47,12 @@ class Rollback extends RollbackCommand
|
||||||
|
|
||||||
event(new DatabaseRolledBack($tenant));
|
event(new DatabaseRolledBack($tenant));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function getTenantCommandName(): string
|
||||||
|
{
|
||||||
|
return 'tenants:rollback';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,36 +5,46 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Commands;
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Contracts\Console\Kernel;
|
||||||
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
|
||||||
class Run extends Command
|
class Run extends Command
|
||||||
{
|
{
|
||||||
/**
|
use HasTenantOptions;
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Run a command for tenant(s)';
|
protected $description = 'Run a command for tenant(s)';
|
||||||
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'tenants:run {commandname : The artisan command.}
|
protected $signature = 'tenants:run {commandname : The artisan command.}
|
||||||
{--tenants=* : The tenant(s) to run the command for. Default: all}';
|
{--tenants=* : The tenant(s) to run the command for. Default: all}';
|
||||||
|
|
||||||
/**
|
public function handle(): int
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
{
|
||||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
$argvInput = $this->argvInput();
|
||||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
|
||||||
|
|
||||||
Artisan::call($this->argument('commandname'));
|
tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) {
|
||||||
$this->comment('Command output:');
|
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||||
$this->info(Artisan::output());
|
|
||||||
|
$this->getLaravel()
|
||||||
|
->make(Kernel::class)
|
||||||
|
->handle($argvInput, new ConsoleOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function argvInput(): ArgvInput
|
||||||
|
{
|
||||||
|
/** @var string $commandName */
|
||||||
|
$commandName = $this->argument('commandname');
|
||||||
|
|
||||||
|
// Convert string command to array
|
||||||
|
$subCommand = explode(' ', $commandName);
|
||||||
|
|
||||||
|
// Add "artisan" as first parameter because ArgvInput expects "artisan" as first parameter and later removes it
|
||||||
|
array_unshift($subCommand, 'artisan');
|
||||||
|
|
||||||
|
return new ArgvInput($subCommand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,37 +6,24 @@ namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Database\ConnectionResolverInterface;
|
use Illuminate\Database\ConnectionResolverInterface;
|
||||||
use Illuminate\Database\Console\Seeds\SeedCommand;
|
use Illuminate\Database\Console\Seeds\SeedCommand;
|
||||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
use Stancl\Tenancy\Events\DatabaseSeeded;
|
use Stancl\Tenancy\Events\DatabaseSeeded;
|
||||||
use Stancl\Tenancy\Events\SeedingDatabase;
|
use Stancl\Tenancy\Events\SeedingDatabase;
|
||||||
|
|
||||||
class Seed extends SeedCommand
|
class Seed extends SeedCommand
|
||||||
{
|
{
|
||||||
use HasATenantsOption;
|
use HasTenantOptions;
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Seed tenant database(s).';
|
protected $description = 'Seed tenant database(s).';
|
||||||
|
|
||||||
protected $name = 'tenants:seed';
|
protected $name = 'tenants:seed';
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new command instance.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(ConnectionResolverInterface $resolver)
|
public function __construct(ConnectionResolverInterface $resolver)
|
||||||
{
|
{
|
||||||
parent::__construct($resolver);
|
parent::__construct($resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function handle(): int
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
{
|
||||||
foreach (config('tenancy.seeder_parameters') as $parameter => $value) {
|
foreach (config('tenancy.seeder_parameters') as $parameter => $value) {
|
||||||
if (! $this->input->hasParameterOption($parameter)) {
|
if (! $this->input->hasParameterOption($parameter)) {
|
||||||
|
|
@ -45,11 +32,11 @@ class Seed extends SeedCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->confirmToProceed()) {
|
if (! $this->confirmToProceed()) {
|
||||||
return;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
|
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||||
|
|
||||||
event(new SeedingDatabase($tenant));
|
event(new SeedingDatabase($tenant));
|
||||||
|
|
||||||
|
|
@ -58,5 +45,7 @@ class Seed extends SeedCommand
|
||||||
|
|
||||||
event(new DatabaseSeeded($tenant));
|
event(new DatabaseSeeded($tenant));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Commands;
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Database\ConnectionResolverInterface;
|
use Illuminate\Database\ConnectionResolverInterface;
|
||||||
use Illuminate\Database\Console\DumpCommand;
|
use Illuminate\Database\Console\DumpCommand;
|
||||||
|
|
@ -23,13 +22,10 @@ class TenantDump extends DumpCommand
|
||||||
|
|
||||||
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int
|
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int
|
||||||
{
|
{
|
||||||
$this->tenant()->run(fn () => parent::handle($connections, $dispatcher));
|
if (is_null($this->option('path'))) {
|
||||||
|
$this->input->setOption('path', database_path('schema/tenant-schema.dump'));
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tenant(): Tenant
|
|
||||||
{
|
|
||||||
$tenant = $this->option('tenant')
|
$tenant = $this->option('tenant')
|
||||||
?? tenant()
|
?? tenant()
|
||||||
?? $this->ask('What tenant do you want to dump the schema for?')
|
?? $this->ask('What tenant do you want to dump the schema for?')
|
||||||
|
|
@ -39,9 +35,15 @@ class TenantDump extends DumpCommand
|
||||||
$tenant = tenancy()->find($tenant);
|
$tenant = tenancy()->find($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw_if(! $tenant, 'Could not identify the tenant to use for dumping the schema.');
|
if ($tenant === null) {
|
||||||
|
$this->components->error('Could not find tenant to use for dumping the schema.');
|
||||||
|
|
||||||
return $tenant;
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::handle($connections, $dispatcher);
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getOptions(): array
|
protected function getOptions(): array
|
||||||
|
|
|
||||||
|
|
@ -5,39 +5,45 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Commands;
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
class TenantList extends Command
|
class TenantList extends Command
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'tenants:list';
|
protected $signature = 'tenants:list';
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'List tenants.';
|
protected $description = 'List tenants.';
|
||||||
|
|
||||||
/**
|
public function handle(): int
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
{
|
||||||
$this->info('Listing all tenants.');
|
$tenants = tenancy()->query()->cursor();
|
||||||
tenancy()
|
|
||||||
->query()
|
$this->components->info("Listing {$tenants->count()} tenants.");
|
||||||
->cursor()
|
|
||||||
->each(function (Tenant $tenant) {
|
foreach ($tenants as $tenant) {
|
||||||
if ($tenant->domains) {
|
/** @var Model&Tenant $tenant */
|
||||||
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? []));
|
$this->components->twoColumnDetail($this->tenantCLI($tenant), $this->domainsCLI($tenant->domains));
|
||||||
} else {
|
|
||||||
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}");
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate the visual CLI output for the tenant name. */
|
||||||
|
protected function tenantCLI(Model&Tenant $tenant): string
|
||||||
|
{
|
||||||
|
return "<fg=yellow>{$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}</>";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate the visual CLI output for the domain names. */
|
||||||
|
protected function domainsCLI(?Collection $domains): ?string
|
||||||
|
{
|
||||||
|
if (! $domains) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "<fg=blue;options=bold>{$domains->pluck('domain')->implode(' / ')}</>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
src/Commands/Up.php
Normal file
29
src/Commands/Up.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
|
|
||||||
|
class Up extends Command
|
||||||
|
{
|
||||||
|
use HasTenantOptions;
|
||||||
|
|
||||||
|
protected $signature = 'tenants:up';
|
||||||
|
|
||||||
|
protected $description = 'Put tenants out of maintenance mode.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||||
|
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||||
|
$tenant->bringUpFromMaintenance();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->components->info('Tenants are now out of maintenance mode.');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,12 @@ namespace Stancl\Tenancy\Concerns;
|
||||||
|
|
||||||
trait DealsWithMigrations
|
trait DealsWithMigrations
|
||||||
{
|
{
|
||||||
protected function getMigrationPaths()
|
protected function getMigrationPaths(): array
|
||||||
{
|
{
|
||||||
if ($this->input->hasOption('path') && $this->input->getOption('path')) {
|
if ($this->input->hasOption('path') && $this->input->getOption('path')) {
|
||||||
return parent::getMigrationPaths();
|
return parent::getMigrationPaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
return database_path('migrations/tenant');
|
return [database_path('migrations/tenant')];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,14 +5,19 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Concerns;
|
namespace Stancl\Tenancy\Concerns;
|
||||||
|
|
||||||
use Illuminate\Support\LazyCollection;
|
use Illuminate\Support\LazyCollection;
|
||||||
|
use Stancl\Tenancy\Database\Concerns\PendingScope;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
trait HasATenantsOption
|
/**
|
||||||
|
* Adds 'tenants' and 'with-pending' options.
|
||||||
|
*/
|
||||||
|
trait HasTenantOptions
|
||||||
{
|
{
|
||||||
protected function getOptions()
|
protected function getOptions()
|
||||||
{
|
{
|
||||||
return array_merge([
|
return array_merge([
|
||||||
['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, '', null],
|
['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, '', null],
|
||||||
|
['with-pending', null, InputOption::VALUE_NONE, 'include pending tenants in query'],
|
||||||
], parent::getOptions());
|
], parent::getOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,6 +28,9 @@ trait HasATenantsOption
|
||||||
->when($this->option('tenants'), function ($query) {
|
->when($this->option('tenants'), function ($query) {
|
||||||
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
|
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
|
||||||
})
|
})
|
||||||
|
->when(tenancy()->model()::hasGlobalScope(PendingScope::class), function ($query) {
|
||||||
|
$query->withPending(config('tenancy.pending.include_in_queries') ?: $this->option('with-pending'));
|
||||||
|
})
|
||||||
->cursor();
|
->cursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Contracts;
|
namespace Stancl\Tenancy\Contracts;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property-read Tenant $tenant
|
* @property-read Tenant $tenant
|
||||||
*
|
*
|
||||||
|
|
@ -15,5 +17,5 @@ namespace Stancl\Tenancy\Contracts;
|
||||||
*/
|
*/
|
||||||
interface Domain
|
interface Domain
|
||||||
{
|
{
|
||||||
public function tenant();
|
public function tenant(): BelongsTo;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,7 @@ interface Syncable
|
||||||
public function getSyncedAttributeNames(): array;
|
public function getSyncedAttributeNames(): array;
|
||||||
|
|
||||||
public function triggerSyncEvent(): void;
|
public function triggerSyncEvent(): void;
|
||||||
|
|
||||||
|
/** Get the attributes used for creating the *other* model (i.e. tenant if this is the central one, and central if this is the tenant one). */
|
||||||
|
public function getSyncedCreationAttributes(): array|null; // todo come up with a better name
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Stancl\Tenancy\Contracts;
|
||||||
*/
|
*/
|
||||||
interface TenancyBootstrapper
|
interface TenancyBootstrapper
|
||||||
{
|
{
|
||||||
public function bootstrap(Tenant $tenant);
|
public function bootstrap(Tenant $tenant): void;
|
||||||
|
|
||||||
public function revert();
|
public function revert(): void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ abstract class TenantCannotBeCreatedException extends \Exception
|
||||||
{
|
{
|
||||||
abstract public function reason(): string;
|
abstract public function reason(): string;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
protected $message;
|
protected $message;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,8 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Contracts;
|
namespace Stancl\Tenancy\Contracts;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Facade\IgnitionContracts\BaseSolution;
|
use Spatie\Ignition\Contracts\BaseSolution;
|
||||||
use Facade\IgnitionContracts\ProvidesSolution;
|
use Spatie\Ignition\Contracts\ProvidesSolution;
|
||||||
use Facade\IgnitionContracts\Solution;
|
|
||||||
|
|
||||||
abstract class TenantCouldNotBeIdentifiedException extends Exception implements ProvidesSolution
|
abstract class TenantCouldNotBeIdentifiedException extends Exception implements ProvidesSolution
|
||||||
{
|
{
|
||||||
|
|
@ -42,7 +41,7 @@ abstract class TenantCouldNotBeIdentifiedException extends Exception implements
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the Ignition description. */
|
/** Get the Ignition description. */
|
||||||
public function getSolution(): Solution
|
public function getSolution(): BaseSolution
|
||||||
{
|
{
|
||||||
return BaseSolution::create($this->solutionTitle)
|
return BaseSolution::create($this->solutionTitle)
|
||||||
->setSolutionDescription($this->solutionDescription)
|
->setSolutionDescription($this->solutionDescription)
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Contracts;
|
namespace Stancl\Tenancy\Contracts;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
interface UniqueIdentifierGenerator
|
interface UniqueIdentifierGenerator
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Generate a unique identifier.
|
* Generate a unique identifier for a model.
|
||||||
*/
|
*/
|
||||||
public static function generate($resource): string;
|
public static function generate(Model $model): string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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
|
trait BelongsToTenant
|
||||||
{
|
{
|
||||||
public static $tenantIdColumn = 'tenant_id';
|
|
||||||
|
|
||||||
public function tenant()
|
public function tenant()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(config('tenancy.tenant_model'), BelongsToTenant::$tenantIdColumn);
|
return $this->belongsTo(config('tenancy.tenant_model'), static::tenantIdColumn());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tenantIdColumn(): string
|
||||||
|
{
|
||||||
|
return config('tenancy.single_db.tenant_id_column');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function bootBelongsToTenant(): void
|
public static function bootBelongsToTenant(): void
|
||||||
|
|
@ -24,9 +27,9 @@ trait BelongsToTenant
|
||||||
static::addGlobalScope(new TenantScope);
|
static::addGlobalScope(new TenantScope);
|
||||||
|
|
||||||
static::creating(function ($model) {
|
static::creating(function ($model) {
|
||||||
if (! $model->getAttribute(BelongsToTenant::$tenantIdColumn) && ! $model->relationLoaded('tenant')) {
|
if (! $model->getAttribute(static::tenantIdColumn()) && ! $model->relationLoaded('tenant')) {
|
||||||
if (tenancy()->initialized) {
|
if (tenancy()->initialized) {
|
||||||
$model->setAttribute(BelongsToTenant::$tenantIdColumn, tenant()->getTenantKey());
|
$model->setAttribute(static::tenantIdColumn(), tenant()->getTenantKey());
|
||||||
$model->setRelation('tenant', tenant());
|
$model->setRelation('tenant', tenant());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Database\Concerns;
|
|
||||||
|
|
||||||
use Stancl\VirtualColumn\VirtualColumn;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extends VirtualColumn for backwards compatibility. This trait will be removed in v4.
|
|
||||||
*/
|
|
||||||
trait HasDataColumn
|
|
||||||
{
|
|
||||||
use VirtualColumn;
|
|
||||||
}
|
|
||||||
|
|
@ -10,6 +10,8 @@ use Stancl\Tenancy\Contracts\Domain;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains
|
* @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains
|
||||||
|
* @mixin \Illuminate\Database\Eloquent\Model
|
||||||
|
* @mixin \Stancl\Tenancy\Contracts\Tenant
|
||||||
*/
|
*/
|
||||||
trait HasDomains
|
trait HasDomains
|
||||||
{
|
{
|
||||||
|
|
|
||||||
103
src/Database/Concerns/HasPending.php
Normal file
103
src/Database/Concerns/HasPending.php
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
use Stancl\Tenancy\Events\CreatingPendingTenant;
|
||||||
|
use Stancl\Tenancy\Events\PendingTenantCreated;
|
||||||
|
use Stancl\Tenancy\Events\PendingTenantPulled;
|
||||||
|
use Stancl\Tenancy\Events\PullingPendingTenant;
|
||||||
|
|
||||||
|
// todo consider adding a method that sets pending_since to null — to flag tenants as not-pending
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Carbon $pending_since
|
||||||
|
*
|
||||||
|
* @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder withPending(bool $withPending = true)
|
||||||
|
* @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder onlyPending()
|
||||||
|
* @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder withoutPending()
|
||||||
|
*/
|
||||||
|
trait HasPending
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Boot the has pending trait for a model.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function bootHasPending()
|
||||||
|
{
|
||||||
|
static::addGlobalScope(new PendingScope());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the has pending trait for an instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function initializeHasPending()
|
||||||
|
{
|
||||||
|
$this->casts['pending_since'] = 'timestamp';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the model instance is in a pending state.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function pending()
|
||||||
|
{
|
||||||
|
return ! is_null($this->pending_since);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a pending tenant. */
|
||||||
|
public static function createPending($attributes = []): Tenant
|
||||||
|
{
|
||||||
|
$tenant = static::create($attributes);
|
||||||
|
|
||||||
|
event(new CreatingPendingTenant($tenant));
|
||||||
|
|
||||||
|
// Update the pending_since value only after the tenant is created so it's
|
||||||
|
// Not marked as pending until finishing running the migrations, seeders, etc.
|
||||||
|
$tenant->update([
|
||||||
|
'pending_since' => now()->timestamp,
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new PendingTenantCreated($tenant));
|
||||||
|
|
||||||
|
return $tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pull a pending tenant. */
|
||||||
|
public static function pullPending(): Tenant
|
||||||
|
{
|
||||||
|
return static::pullPendingFromPool(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try to pull a tenant from the pool of pending tenants. */
|
||||||
|
public static function pullPendingFromPool(bool $firstOrCreate = false): ?Tenant
|
||||||
|
{
|
||||||
|
if (! static::onlyPending()->exists()) {
|
||||||
|
if (! $firstOrCreate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static::createPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pending tenant is surely available at this point
|
||||||
|
$tenant = static::onlyPending()->first();
|
||||||
|
|
||||||
|
event(new PullingPendingTenant($tenant));
|
||||||
|
|
||||||
|
$tenant->update([
|
||||||
|
'pending_since' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new PendingTenantPulled($tenant));
|
||||||
|
|
||||||
|
return $tenant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,11 +11,11 @@ trait HasScopedValidationRules
|
||||||
{
|
{
|
||||||
public function unique($table, $column = 'NULL')
|
public function unique($table, $column = 'NULL')
|
||||||
{
|
{
|
||||||
return (new Unique($table, $column))->where(BelongsToTenant::$tenantIdColumn, $this->getTenantKey());
|
return (new Unique($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exists($table, $column = 'NULL')
|
public function exists($table, $column = 'NULL')
|
||||||
{
|
{
|
||||||
return (new Exists($table, $column))->where(BelongsToTenant::$tenantIdColumn, $this->getTenantKey());
|
return (new Exists($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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;
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Resolvers;
|
|
||||||
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
|
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
trait InvalidatesResolverCache
|
trait InvalidatesResolverCache
|
||||||
{
|
{
|
||||||
public static $resolvers = [
|
|
||||||
Resolvers\DomainTenantResolver::class,
|
|
||||||
Resolvers\PathTenantResolver::class,
|
|
||||||
Resolvers\RequestDataTenantResolver::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
public static function bootInvalidatesResolverCache(): void
|
public static function bootInvalidatesResolverCache(): void
|
||||||
{
|
{
|
||||||
static::saved(function (Tenant $tenant) {
|
static::saved(function (Tenant $tenant) {
|
||||||
foreach (static::$resolvers as $resolver) {
|
foreach (Tenancy::cachedResolvers() as $resolver) {
|
||||||
/** @var CachedTenantResolver $resolver */
|
/** @var CachedTenantResolver $resolver */
|
||||||
$resolver = app($resolver);
|
$resolver = app($resolver);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,18 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Database\Concerns;
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Stancl\Tenancy\Resolvers;
|
|
||||||
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
|
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meant to be used on models that belong to tenants.
|
* Meant to be used on models that belong to tenants.
|
||||||
*/
|
*/
|
||||||
trait InvalidatesTenantsResolverCache
|
trait InvalidatesTenantsResolverCache
|
||||||
{
|
{
|
||||||
public static $resolvers = [
|
|
||||||
Resolvers\DomainTenantResolver::class,
|
|
||||||
Resolvers\PathTenantResolver::class,
|
|
||||||
Resolvers\RequestDataTenantResolver::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
public static function bootInvalidatesTenantsResolverCache(): void
|
public static function bootInvalidatesTenantsResolverCache(): void
|
||||||
{
|
{
|
||||||
static::saved(function (Model $model) {
|
static::saved(function (Model $model) {
|
||||||
foreach (static::$resolvers as $resolver) {
|
foreach (Tenancy::cachedResolvers() as $resolver) {
|
||||||
/** @var CachedTenantResolver $resolver */
|
/** @var CachedTenantResolver $resolver */
|
||||||
$resolver = app($resolver);
|
$resolver = app($resolver);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,34 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Database\Concerns;
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Stancl\Tenancy\Events\TenantMaintenanceModeDisabled;
|
||||||
|
use Stancl\Tenancy\Events\TenantMaintenanceModeEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \Illuminate\Database\Eloquent\Model
|
||||||
|
*/
|
||||||
trait MaintenanceMode
|
trait MaintenanceMode
|
||||||
{
|
{
|
||||||
public function putDownForMaintenance($data = [])
|
public function putDownForMaintenance($data = []): void
|
||||||
{
|
{
|
||||||
$this->update(['maintenance_mode' => [
|
$this->update([
|
||||||
'time' => $data['time'] ?? Carbon::now()->getTimestamp(),
|
'maintenance_mode' => [
|
||||||
'message' => $data['message'] ?? null,
|
'except' => $data['except'] ?? null,
|
||||||
|
'redirect' => $data['redirect'] ?? null,
|
||||||
'retry' => $data['retry'] ?? null,
|
'retry' => $data['retry'] ?? null,
|
||||||
'allowed' => $data['allowed'] ?? [],
|
'refresh' => $data['refresh'] ?? null,
|
||||||
]]);
|
'secret' => $data['secret'] ?? null,
|
||||||
|
'status' => $data['status'] ?? 503,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new TenantMaintenanceModeEnabled($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bringUpFromMaintenance(): void
|
||||||
|
{
|
||||||
|
$this->update(['maintenance_mode' => null]);
|
||||||
|
|
||||||
|
event(new TenantMaintenanceModeDisabled($this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
88
src/Database/Concerns/PendingScope.php
Normal file
88
src/Database/Concerns/PendingScope.php
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Scope;
|
||||||
|
|
||||||
|
class PendingScope implements Scope
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* All of the extensions to be added to the builder.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $extensions = ['WithPending', 'WithoutPending', 'OnlyPending'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the scope to a given Eloquent query builder.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function apply(Builder $builder, Model $model)
|
||||||
|
{
|
||||||
|
$builder->when(! config('tenancy.pending.include_in_queries'), function (Builder $builder) {
|
||||||
|
$builder->whereNull($builder->getModel()->getColumnForQuery('pending_since'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the query builder with the needed functions.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function extend(Builder $builder)
|
||||||
|
{
|
||||||
|
foreach ($this->extensions as $extension) {
|
||||||
|
$this->{"add{$extension}"}($builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add the with-pending extension to the builder.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addWithPending(Builder $builder)
|
||||||
|
{
|
||||||
|
$builder->macro('withPending', function (Builder $builder, $withPending = true) {
|
||||||
|
if (! $withPending) {
|
||||||
|
return $builder->withoutPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder->withoutGlobalScope($this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the without-pending extension to the builder.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addWithoutPending(Builder $builder)
|
||||||
|
{
|
||||||
|
$builder->macro('withoutPending', function (Builder $builder) {
|
||||||
|
$builder->withoutGlobalScope($this)
|
||||||
|
->whereNull($builder->getModel()->getColumnForQuery('pending_since'))
|
||||||
|
->orWhereNull($builder->getModel()->getDataColumn());
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the only-pending extension to the builder.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addOnlyPending(Builder $builder)
|
||||||
|
{
|
||||||
|
$builder->macro('onlyPending', function (Builder $builder) {
|
||||||
|
$builder->withoutGlobalScope($this)->whereNotNull($builder->getModel()->getColumnForQuery('pending_since'));
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,4 +32,9 @@ trait ResourceSyncing
|
||||||
/** @var Syncable $this */
|
/** @var Syncable $this */
|
||||||
event(new SyncedResourceSaved($this, tenant()));
|
event(new SyncedResourceSaved($this, tenant()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSyncedCreationAttributes(): array|null
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/Database/Contracts/StatefulTenantDatabaseManager.php
Normal file
24
src/Database/Contracts/StatefulTenantDatabaseManager.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Database\Contracts;
|
||||||
|
|
||||||
|
use Illuminate\Database\Connection;
|
||||||
|
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tenant database manager with a persistent connection.
|
||||||
|
*/
|
||||||
|
interface StatefulTenantDatabaseManager extends TenantDatabaseManager
|
||||||
|
{
|
||||||
|
/** Get the DB connection used by the tenant database manager. */
|
||||||
|
public function database(): Connection; // todo rename to connection()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the DB connection that should be used by the tenant database manager.
|
||||||
|
*
|
||||||
|
* @throws NoConnectionSetException
|
||||||
|
*/
|
||||||
|
public function setConnection(string $connection): void;
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Database\Contracts;
|
namespace Stancl\Tenancy\Database\Contracts;
|
||||||
|
|
||||||
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
|
|
||||||
|
|
||||||
interface TenantDatabaseManager
|
interface TenantDatabaseManager
|
||||||
{
|
{
|
||||||
/** Create a database. */
|
/** Create a database. */
|
||||||
|
|
@ -19,11 +17,4 @@ interface TenantDatabaseManager
|
||||||
|
|
||||||
/** Construct a DB connection config array. */
|
/** Construct a DB connection config array. */
|
||||||
public function makeConnectionConfig(array $baseConfig, string $databaseName): array;
|
public function makeConnectionConfig(array $baseConfig, string $databaseName): array;
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the DB connection that should be used by the tenant database manager.
|
|
||||||
*
|
|
||||||
* @throws NoConnectionSetException
|
|
||||||
*/
|
|
||||||
public function setConnection(string $connection): void;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,15 @@ use Stancl\Tenancy\Database\DatabaseConfig;
|
||||||
|
|
||||||
interface TenantWithDatabase extends Tenant
|
interface TenantWithDatabase extends Tenant
|
||||||
{
|
{
|
||||||
|
/** Get the tenant's database config. */
|
||||||
public function database(): DatabaseConfig;
|
public function database(): DatabaseConfig;
|
||||||
|
|
||||||
|
/** Get the internal prefix. */
|
||||||
|
public static function internalPrefix(): string;
|
||||||
|
|
||||||
|
/** Get an internal key. */
|
||||||
|
public function getInternal(string $key): mixed;
|
||||||
|
|
||||||
|
/** Set internal key. */
|
||||||
|
public function setInternal(string $key, mixed $value): static;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Database;
|
namespace Stancl\Tenancy\Database;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Illuminate\Database;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase as Tenant;
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase as Tenant;
|
||||||
|
use Stancl\Tenancy\Database\Exceptions\DatabaseManagerNotRegisteredException;
|
||||||
|
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
|
||||||
|
|
||||||
class DatabaseConfig
|
class DatabaseConfig
|
||||||
{
|
{
|
||||||
|
|
@ -26,20 +30,20 @@ class DatabaseConfig
|
||||||
|
|
||||||
public static function __constructStatic(): void
|
public static function __constructStatic(): void
|
||||||
{
|
{
|
||||||
static::$usernameGenerator = static::$usernameGenerator ?? function (Tenant $tenant) {
|
static::$usernameGenerator = static::$usernameGenerator ?? function (Model&Tenant $tenant) {
|
||||||
return Str::random(16);
|
return Str::random(16);
|
||||||
};
|
};
|
||||||
|
|
||||||
static::$passwordGenerator = static::$passwordGenerator ?? function (Tenant $tenant) {
|
static::$passwordGenerator = static::$passwordGenerator ?? function (Model&Tenant $tenant) {
|
||||||
return Hash::make(Str::random(32));
|
return Hash::make(Str::random(32));
|
||||||
};
|
};
|
||||||
|
|
||||||
static::$databaseNameGenerator = static::$databaseNameGenerator ?? function (Tenant $tenant) {
|
static::$databaseNameGenerator = static::$databaseNameGenerator ?? function (Model&Tenant $tenant) {
|
||||||
return config('tenancy.database.prefix') . $tenant->getTenantKey() . config('tenancy.database.suffix');
|
return config('tenancy.database.prefix') . $tenant->getTenantKey() . config('tenancy.database.suffix');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __construct(Tenant $tenant)
|
public function __construct(Model&Tenant $tenant)
|
||||||
{
|
{
|
||||||
static::__constructStatic();
|
static::__constructStatic();
|
||||||
|
|
||||||
|
|
@ -61,7 +65,7 @@ class DatabaseConfig
|
||||||
static::$passwordGenerator = $passwordGenerator;
|
static::$passwordGenerator = $passwordGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getName(): ?string
|
public function getName(): string
|
||||||
{
|
{
|
||||||
return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant);
|
return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant);
|
||||||
}
|
}
|
||||||
|
|
@ -81,9 +85,9 @@ class DatabaseConfig
|
||||||
*/
|
*/
|
||||||
public function makeCredentials(): void
|
public function makeCredentials(): void
|
||||||
{
|
{
|
||||||
$this->tenant->setInternal('db_name', $this->getName() ?? (static::$databaseNameGenerator)($this->tenant));
|
$this->tenant->setInternal('db_name', $this->getName());
|
||||||
|
|
||||||
if ($this->manager() instanceof Contracts\ManagesDatabaseUsers) {
|
if ($this->connectionDriverManager($this->getTemplateConnectionName()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||||
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));
|
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));
|
||||||
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant));
|
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant));
|
||||||
}
|
}
|
||||||
|
|
@ -100,6 +104,11 @@ class DatabaseConfig
|
||||||
?? config('tenancy.database.central_connection');
|
?? config('tenancy.database.central_connection');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTenantHostConnectionName(): string
|
||||||
|
{
|
||||||
|
return config('tenancy.database.tenant_host_connection_name', 'tenant_host_connection');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenant's own database connection config.
|
* Tenant's own database connection config.
|
||||||
*/
|
*/
|
||||||
|
|
@ -114,6 +123,40 @@ class DatabaseConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tenant's host database connection config.
|
||||||
|
*/
|
||||||
|
public function hostConnection(): array
|
||||||
|
{
|
||||||
|
$config = $this->tenantConfig();
|
||||||
|
$template = $this->getTemplateConnectionName();
|
||||||
|
$templateConnection = config("database.connections.{$template}");
|
||||||
|
|
||||||
|
if ($this->connectionDriverManager($template) instanceof Contracts\ManagesDatabaseUsers) {
|
||||||
|
// We're removing the username and password because user with these credentials is not created yet
|
||||||
|
// If you need to provide username and password when using PermissionControlledMySQLDatabaseManager,
|
||||||
|
// consider creating a new connection and use it as `tenancy_db_connection` tenant config key
|
||||||
|
unset($config['username'], $config['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $config) {
|
||||||
|
return $templateConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_replace($templateConnection, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge host database connection.
|
||||||
|
*
|
||||||
|
* It's possible database has previous tenant connection.
|
||||||
|
* This will clean up the previous connection before creating it for the current tenant.
|
||||||
|
*/
|
||||||
|
public function purgeHostConnection(): void
|
||||||
|
{
|
||||||
|
DB::purge($this->getTenantHostConnectionName());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional config for the database connection, specific to this tenant.
|
* Additional config for the database connection, specific to this tenant.
|
||||||
*/
|
*/
|
||||||
|
|
@ -140,10 +183,37 @@ class DatabaseConfig
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the TenantDatabaseManager for this tenant's connection. */
|
/** Get the TenantDatabaseManager for this tenant's connection.
|
||||||
|
*
|
||||||
|
* @throws NoConnectionSetException|DatabaseManagerNotRegisteredException
|
||||||
|
*/
|
||||||
public function manager(): Contracts\TenantDatabaseManager
|
public function manager(): Contracts\TenantDatabaseManager
|
||||||
{
|
{
|
||||||
$driver = config("database.connections.{$this->getTemplateConnectionName()}.driver");
|
// Laravel caches the previous PDO connection, so we purge it to be able to change the connection details
|
||||||
|
$this->purgeHostConnection(); // todo come up with a better name
|
||||||
|
|
||||||
|
// Create the tenant host connection config
|
||||||
|
$tenantHostConnectionName = $this->getTenantHostConnectionName();
|
||||||
|
config(["database.connections.{$tenantHostConnectionName}" => $this->hostConnection()]);
|
||||||
|
|
||||||
|
$manager = $this->connectionDriverManager($tenantHostConnectionName);
|
||||||
|
|
||||||
|
if ($manager instanceof Contracts\StatefulTenantDatabaseManager) {
|
||||||
|
$manager->setConnection($tenantHostConnectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* todo come up with a better name
|
||||||
|
* Get database manager class from the given connection config's driver.
|
||||||
|
*
|
||||||
|
* @throws DatabaseManagerNotRegisteredException
|
||||||
|
*/
|
||||||
|
protected function connectionDriverManager(string $connectionName): Contracts\TenantDatabaseManager
|
||||||
|
{
|
||||||
|
$driver = config("database.connections.{$connectionName}.driver");
|
||||||
|
|
||||||
$databaseManagers = config('tenancy.database.managers');
|
$databaseManagers = config('tenancy.database.managers');
|
||||||
|
|
||||||
|
|
@ -151,11 +221,6 @@ class DatabaseConfig
|
||||||
throw new Exceptions\DatabaseManagerNotRegisteredException($driver);
|
throw new Exceptions\DatabaseManagerNotRegisteredException($driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var Contracts\TenantDatabaseManager $databaseManager */
|
return app($databaseManagers[$driver]);
|
||||||
$databaseManager = app($databaseManagers[$driver]);
|
|
||||||
|
|
||||||
$databaseManager->setConnection($this->getTemplateConnectionName());
|
|
||||||
|
|
||||||
return $databaseManager;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,25 +15,14 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
*/
|
*/
|
||||||
class DatabaseManager
|
class DatabaseManager
|
||||||
{
|
{
|
||||||
/** @var Application */
|
public function __construct(
|
||||||
protected $app;
|
protected Application $app,
|
||||||
|
protected BaseDatabaseManager $database,
|
||||||
/** @var BaseDatabaseManager */
|
protected Repository $config,
|
||||||
protected $database;
|
) {
|
||||||
|
|
||||||
/** @var Repository */
|
|
||||||
protected $config;
|
|
||||||
|
|
||||||
public function __construct(Application $app, BaseDatabaseManager $database, Repository $config)
|
|
||||||
{
|
|
||||||
$this->app = $app;
|
|
||||||
$this->database = $database;
|
|
||||||
$this->config = $config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Connect to a tenant's database. */
|
||||||
* Connect to a tenant's database.
|
|
||||||
*/
|
|
||||||
public function connectToTenant(TenantWithDatabase $tenant): void
|
public function connectToTenant(TenantWithDatabase $tenant): void
|
||||||
{
|
{
|
||||||
$this->purgeTenantConnection();
|
$this->purgeTenantConnection();
|
||||||
|
|
@ -41,35 +30,27 @@ class DatabaseManager
|
||||||
$this->setDefaultConnection('tenant');
|
$this->setDefaultConnection('tenant');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Reconnect to the default non-tenant connection. */
|
||||||
* Reconnect to the default non-tenant connection.
|
|
||||||
*/
|
|
||||||
public function reconnectToCentral(): void
|
public function reconnectToCentral(): void
|
||||||
{
|
{
|
||||||
$this->purgeTenantConnection();
|
$this->purgeTenantConnection();
|
||||||
$this->setDefaultConnection($this->config->get('tenancy.database.central_connection'));
|
$this->setDefaultConnection($this->config->get('tenancy.database.central_connection'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Change the default database connection config. */
|
||||||
* Change the default database connection config.
|
|
||||||
*/
|
|
||||||
public function setDefaultConnection(string $connection): void
|
public function setDefaultConnection(string $connection): void
|
||||||
{
|
{
|
||||||
$this->config['database.default'] = $connection;
|
$this->config['database.default'] = $connection;
|
||||||
$this->database->setDefaultConnection($connection);
|
$this->database->setDefaultConnection($connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Create the tenant database connection. */
|
||||||
* Create the tenant database connection.
|
|
||||||
*/
|
|
||||||
public function createTenantConnection(TenantWithDatabase $tenant): void
|
public function createTenantConnection(TenantWithDatabase $tenant): void
|
||||||
{
|
{
|
||||||
$this->config['database.connections.tenant'] = $tenant->database()->connection();
|
$this->config['database.connections.tenant'] = $tenant->database()->connection();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Purge the tenant database connection. */
|
||||||
* Purge the tenant database connection.
|
|
||||||
*/
|
|
||||||
public function purgeTenantConnection(): void
|
public function purgeTenantConnection(): void
|
||||||
{
|
{
|
||||||
if (array_key_exists('tenant', $this->database->getConnections())) {
|
if (array_key_exists('tenant', $this->database->getConnections())) {
|
||||||
|
|
@ -83,8 +64,8 @@ class DatabaseManager
|
||||||
* Check if a tenant can be created.
|
* Check if a tenant can be created.
|
||||||
*
|
*
|
||||||
* @throws TenantCannotBeCreatedException
|
* @throws TenantCannotBeCreatedException
|
||||||
* @throws DatabaseManagerNotRegisteredException
|
* @throws Exceptions\DatabaseManagerNotRegisteredException
|
||||||
* @throws TenantDatabaseAlreadyExistsException
|
* @throws Exceptions\TenantDatabaseAlreadyExistsException
|
||||||
*/
|
*/
|
||||||
public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void
|
public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void
|
||||||
{
|
{
|
||||||
|
|
@ -94,8 +75,13 @@ class DatabaseManager
|
||||||
throw new Exceptions\TenantDatabaseAlreadyExistsException($database);
|
throw new Exceptions\TenantDatabaseAlreadyExistsException($database);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($manager instanceof Contracts\ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) {
|
if ($manager instanceof Contracts\ManagesDatabaseUsers) {
|
||||||
|
/** @var string $username */
|
||||||
|
$username = $tenant->database()->getUsername();
|
||||||
|
|
||||||
|
if ($manager->userExists($username)) {
|
||||||
throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username);
|
throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Database\Models;
|
namespace Stancl\Tenancy\Database\Models;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Stancl\Tenancy\Database\Concerns\CentralConnection;
|
use Stancl\Tenancy\Database\Concerns\CentralConnection;
|
||||||
|
use Stancl\Tenancy\Exceptions\StatefulGuardRequiredException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $token
|
* @property string $token
|
||||||
|
|
@ -38,9 +41,15 @@ class ImpersonationToken extends Model
|
||||||
public static function booted(): void
|
public static function booted(): void
|
||||||
{
|
{
|
||||||
static::creating(function ($model) {
|
static::creating(function ($model) {
|
||||||
|
$authGuard = $model->auth_guard ?? config('auth.defaults.guard');
|
||||||
|
|
||||||
|
if (! Auth::guard($authGuard) instanceof StatefulGuard) {
|
||||||
|
throw new StatefulGuardRequiredException($authGuard);
|
||||||
|
}
|
||||||
|
|
||||||
$model->created_at = $model->created_at ?? $model->freshTimestamp();
|
$model->created_at = $model->created_at ?? $model->freshTimestamp();
|
||||||
$model->token = $model->token ?? Str::random(128);
|
$model->token = $model->token ?? Str::random(128);
|
||||||
$model->auth_guard = $model->auth_guard ?? config('auth.defaults.guard');
|
$model->auth_guard = $authGuard;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ use Stancl\Tenancy\Contracts;
|
||||||
use Stancl\Tenancy\Database\Concerns;
|
use Stancl\Tenancy\Database\Concerns;
|
||||||
use Stancl\Tenancy\Database\TenantCollection;
|
use Stancl\Tenancy\Database\TenantCollection;
|
||||||
use Stancl\Tenancy\Events;
|
use Stancl\Tenancy\Events;
|
||||||
|
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
||||||
|
use Stancl\VirtualColumn\VirtualColumn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string|int $id
|
* @property string|int $id
|
||||||
|
|
@ -21,17 +23,17 @@ use Stancl\Tenancy\Events;
|
||||||
*/
|
*/
|
||||||
class Tenant extends Model implements Contracts\Tenant
|
class Tenant extends Model implements Contracts\Tenant
|
||||||
{
|
{
|
||||||
use Concerns\CentralConnection,
|
use VirtualColumn,
|
||||||
|
Concerns\CentralConnection,
|
||||||
Concerns\GeneratesIds,
|
Concerns\GeneratesIds,
|
||||||
Concerns\HasDataColumn,
|
|
||||||
Concerns\HasInternalKeys,
|
Concerns\HasInternalKeys,
|
||||||
Concerns\TenantRun,
|
Concerns\TenantRun,
|
||||||
|
Concerns\HasPending,
|
||||||
|
Concerns\InitializationHelpers,
|
||||||
Concerns\InvalidatesResolverCache;
|
Concerns\InvalidatesResolverCache;
|
||||||
|
|
||||||
protected $table = 'tenants';
|
protected $table = 'tenants';
|
||||||
|
|
||||||
protected $primaryKey = 'id';
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
public function getTenantKeyName(): string
|
public function getTenantKeyName(): string
|
||||||
|
|
@ -44,6 +46,22 @@ class Tenant extends Model implements Contracts\Tenant
|
||||||
return $this->getAttribute($this->getTenantKeyName());
|
return $this->getAttribute($this->getTenantKeyName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the current tenant. */
|
||||||
|
public static function current(): static|null
|
||||||
|
{
|
||||||
|
return tenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current tenant or throw an exception if tenancy is not initialized.
|
||||||
|
*
|
||||||
|
* @throws TenancyNotInitializedException
|
||||||
|
*/
|
||||||
|
public static function currentOrFail(): static
|
||||||
|
{
|
||||||
|
return static::current() ?? throw new TenancyNotInitializedException;
|
||||||
|
}
|
||||||
|
|
||||||
public function newCollection(array $models = []): TenantCollection
|
public function newCollection(array $models = []): TenantCollection
|
||||||
{
|
{
|
||||||
return new TenantCollection($models);
|
return new TenantCollection($models);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class ParentModelScope implements Scope
|
||||||
$builder->whereHas($builder->getModel()->getRelationshipToPrimaryModel());
|
$builder->whereHas($builder->getModel()->getRelationshipToPrimaryModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function extend(Builder $builder)
|
public function extend(Builder $builder): void
|
||||||
{
|
{
|
||||||
$builder->macro('withoutParentModel', function (Builder $builder) {
|
$builder->macro('withoutParentModel', function (Builder $builder) {
|
||||||
return $builder->withoutGlobalScope($this);
|
return $builder->withoutGlobalScope($this);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
|
||||||
{
|
{
|
||||||
use CreatesDatabaseUsers;
|
use CreatesDatabaseUsers;
|
||||||
|
|
||||||
public static $grants = [
|
/** @var string[] */
|
||||||
|
public static array $grants = [
|
||||||
'ALTER', 'ALTER ROUTINE', 'CREATE', 'CREATE ROUTINE', 'CREATE TEMPORARY TABLES', 'CREATE VIEW',
|
'ALTER', 'ALTER ROUTINE', 'CREATE', 'CREATE ROUTINE', 'CREATE TEMPORARY TABLES', 'CREATE VIEW',
|
||||||
'DELETE', 'DROP', 'EVENT', 'EXECUTE', 'INDEX', 'INSERT', 'LOCK TABLES', 'REFERENCES', 'SELECT',
|
'DELETE', 'DROP', 'EVENT', 'EXECUTE', 'INDEX', 'INSERT', 'LOCK TABLES', 'REFERENCES', 'SELECT',
|
||||||
'SHOW VIEW', 'TRIGGER', 'UPDATE',
|
'SHOW VIEW', 'TRIGGER', 'UPDATE',
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,7 @@ class PostgreSQLSchemaManager extends TenantDatabaseManager
|
||||||
|
|
||||||
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
|
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
|
||||||
{
|
{
|
||||||
if (version_compare(app()->version(), '9.0', '>=')) {
|
|
||||||
$baseConfig['search_path'] = $databaseName;
|
$baseConfig['search_path'] = $databaseName;
|
||||||
} else {
|
|
||||||
$baseConfig['schema'] = $databaseName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $baseConfig;
|
return $baseConfig;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,15 @@ use Throwable;
|
||||||
|
|
||||||
class SQLiteDatabaseManager implements TenantDatabaseManager
|
class SQLiteDatabaseManager implements TenantDatabaseManager
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* SQLite Database path without ending slash.
|
||||||
|
*/
|
||||||
|
public static string|null $path = null;
|
||||||
|
|
||||||
public function createDatabase(TenantWithDatabase $tenant): bool
|
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return file_put_contents(database_path($tenant->database()->getName()), '');
|
return (bool) file_put_contents($this->getPath($tenant->database()->getName()), '');
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +27,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
||||||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return unlink(database_path($tenant->database()->getName()));
|
return unlink($this->getPath($tenant->database()->getName()));
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +35,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
||||||
|
|
||||||
public function databaseExists(string $name): bool
|
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
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@ namespace Stancl\Tenancy\Database\TenantDatabaseManagers;
|
||||||
|
|
||||||
use Illuminate\Database\Connection;
|
use Illuminate\Database\Connection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager as Contract;
|
use Stancl\Tenancy\Database\Contracts\StatefulTenantDatabaseManager;
|
||||||
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
|
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
|
||||||
|
|
||||||
abstract class TenantDatabaseManager implements Contract // todo better naming?
|
abstract class TenantDatabaseManager implements StatefulTenantDatabaseManager
|
||||||
{
|
{
|
||||||
/** The database connection to the server. */
|
/** The database connection to the server. */
|
||||||
protected string $connection;
|
protected string $connection;
|
||||||
|
|
||||||
protected function database(): Connection
|
public function database(): Connection
|
||||||
{
|
{
|
||||||
if (! isset($this->connection)) {
|
if (! isset($this->connection)) {
|
||||||
throw new NoConnectionSetException(static::class);
|
throw new NoConnectionSetException(static::class);
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ class TenantScope implements Scope
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder->where($model->qualifyColumn(BelongsToTenant::$tenantIdColumn), tenant()->getTenantKey());
|
$builder->where($model->qualifyColumn(BelongsToTenant::tenantIdColumn()), tenant()->getTenantKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function extend(Builder $builder)
|
public function extend(Builder $builder): void
|
||||||
{
|
{
|
||||||
$builder->macro('withoutTenancy', function (Builder $builder) {
|
$builder->macro('withoutTenancy', function (Builder $builder) {
|
||||||
return $builder->withoutGlobalScope($this);
|
return $builder->withoutGlobalScope($this);
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,8 @@ use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
abstract class TenancyEvent
|
abstract class TenancyEvent
|
||||||
{
|
{
|
||||||
/** @var Tenancy */
|
public function __construct(
|
||||||
public $tenancy;
|
public Tenancy $tenancy,
|
||||||
|
) {
|
||||||
public function __construct(Tenancy $tenancy)
|
|
||||||
{
|
|
||||||
$this->tenancy = $tenancy;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Events\Contracts;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
abstract class TenantEvent
|
abstract class TenantEvent // todo we could add a feature to JobPipeline that automatically gets data for the send() from here
|
||||||
{
|
{
|
||||||
use SerializesModels;
|
use SerializesModels;
|
||||||
|
|
||||||
|
|
|
||||||
9
src/Events/CreatingPendingTenant.php
Normal file
9
src/Events/CreatingPendingTenant.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class CreatingPendingTenant extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
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/PendingTenantCreated.php
Normal file
9
src/Events/PendingTenantCreated.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class PendingTenantCreated extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
9
src/Events/PendingTenantPulled.php
Normal file
9
src/Events/PendingTenantPulled.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class PendingTenantPulled extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
9
src/Events/PullingPendingTenant.php
Normal file
9
src/Events/PullingPendingTenant.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class PullingPendingTenant 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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -10,14 +10,9 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
|
|
||||||
class SyncedResourceSaved
|
class SyncedResourceSaved
|
||||||
{
|
{
|
||||||
public Syncable&Model $model;
|
public function __construct(
|
||||||
|
public Syncable&Model $model,
|
||||||
/** @var (TenantWithDatabase&Model)|null */
|
public TenantWithDatabase|null $tenant,
|
||||||
public TenantWithDatabase|null $tenant;
|
) {
|
||||||
|
|
||||||
public function __construct(Syncable $model, TenantWithDatabase|null $tenant)
|
|
||||||
{
|
|
||||||
$this->model = $model;
|
|
||||||
$this->tenant = $tenant;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
src/Events/TenantMaintenanceModeDisabled.php
Normal file
10
src/Events/TenantMaintenanceModeDisabled.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class TenantMaintenanceModeDisabled extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
10
src/Events/TenantMaintenanceModeEnabled.php
Normal file
10
src/Events/TenantMaintenanceModeEnabled.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
class TenantMaintenanceModeEnabled extends Contracts\TenantEvent
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ use Exception;
|
||||||
|
|
||||||
class DomainOccupiedByOtherTenantException extends Exception
|
class DomainOccupiedByOtherTenantException extends Exception
|
||||||
{
|
{
|
||||||
public function __construct($domain)
|
public function __construct(string $domain)
|
||||||
{
|
{
|
||||||
parent::__construct("The $domain domain is occupied by another tenant.");
|
parent::__construct("The $domain domain is occupied by another tenant.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ class RouteIsMissingTenantParameterException extends Exception
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$parameter = PathTenantResolver::$tenantParameterName;
|
$parameter = PathTenantResolver::tenantParameterName();
|
||||||
|
|
||||||
parent::__construct("The route's first argument is not the tenant id (configured paramter name: $parameter).");
|
parent::__construct("The route's first argument is not the tenant id (configured paramter name: $parameter).");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/Exceptions/StatefulGuardRequiredException.php
Normal file
15
src/Exceptions/StatefulGuardRequiredException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class StatefulGuardRequiredException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $guardName)
|
||||||
|
{
|
||||||
|
parent::__construct("Cannot use a non-stateful guard ('$guardName'). A guard implementing the Illuminate\\Contracts\\Auth\\StatefulGuard interface is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ use Exception;
|
||||||
|
|
||||||
class TenancyNotInitializedException extends Exception
|
class TenancyNotInitializedException extends Exception
|
||||||
{
|
{
|
||||||
public function __construct($message = '')
|
public function __construct(string $message = '')
|
||||||
{
|
{
|
||||||
parent::__construct($message ?: 'Tenancy is not initialized.');
|
parent::__construct($message ?: 'Tenancy is not initialized.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,16 @@ class CrossDomainRedirect implements Feature
|
||||||
{
|
{
|
||||||
RedirectResponse::macro('domain', function (string $domain) {
|
RedirectResponse::macro('domain', function (string $domain) {
|
||||||
/** @var RedirectResponse $this */
|
/** @var RedirectResponse $this */
|
||||||
|
|
||||||
// Replace first occurrence of the hostname fragment with $domain
|
|
||||||
$url = $this->getTargetUrl();
|
$url = $this->getTargetUrl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original hostname in the redirect response.
|
||||||
|
*
|
||||||
|
* @var string $hostname
|
||||||
|
*/
|
||||||
$hostname = parse_url($url, PHP_URL_HOST);
|
$hostname = parse_url($url, PHP_URL_HOST);
|
||||||
$position = strpos($url, $hostname);
|
|
||||||
$this->setTargetUrl(substr_replace($url, $domain, $position, strlen($hostname)));
|
$this->setTargetUrl((string) str($url)->replace($hostname, $domain));
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,25 @@ use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
class TenantConfig implements Feature
|
class TenantConfig implements Feature
|
||||||
{
|
{
|
||||||
/** @var Repository */
|
|
||||||
protected $config;
|
|
||||||
|
|
||||||
public array $originalConfig = [];
|
public array $originalConfig = [];
|
||||||
|
|
||||||
public static $storageToConfigMap = [
|
/** @var array<string, string|array> */
|
||||||
|
public static array $storageToConfigMap = [
|
||||||
// 'paypal_api_key' => 'services.paypal.api_key',
|
// 'paypal_api_key' => 'services.paypal.api_key',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(Repository $config)
|
public function __construct(
|
||||||
{
|
protected Repository $config,
|
||||||
$this->config = $config;
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
public function bootstrap(Tenancy $tenancy): void
|
||||||
{
|
{
|
||||||
Event::listen(TenancyBootstrapped::class, function (TenancyBootstrapped $event) {
|
Event::listen(TenancyBootstrapped::class, function (TenancyBootstrapped $event) {
|
||||||
$this->setTenantConfig($event->tenancy->tenant);
|
/** @var Tenant $tenant */
|
||||||
|
$tenant = $event->tenancy->tenant;
|
||||||
|
|
||||||
|
$this->setTenantConfig($tenant);
|
||||||
});
|
});
|
||||||
|
|
||||||
Event::listen(RevertedToCentralContext::class, function () {
|
Event::listen(RevertedToCentralContext::class, function () {
|
||||||
|
|
@ -43,8 +44,8 @@ class TenantConfig implements Feature
|
||||||
|
|
||||||
public function setTenantConfig(Tenant $tenant): void
|
public function setTenantConfig(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
/** @var Tenant|Model $tenant */
|
|
||||||
foreach (static::$storageToConfigMap as $storageKey => $configKey) {
|
foreach (static::$storageToConfigMap as $storageKey => $configKey) {
|
||||||
|
/** @var Tenant&Model $tenant */
|
||||||
$override = Arr::get($tenant, $storageKey);
|
$override = Arr::get($tenant, $storageKey);
|
||||||
|
|
||||||
if (! is_null($override)) {
|
if (! is_null($override)) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ class UniversalRoutes implements Feature
|
||||||
public static string $middlewareGroup = 'universal';
|
public static string $middlewareGroup = 'universal';
|
||||||
|
|
||||||
// todo docblock
|
// todo docblock
|
||||||
|
/** @var array<class-string<\Stancl\Tenancy\Middleware\IdentificationMiddleware>> */
|
||||||
public static array $identificationMiddlewares = [
|
public static array $identificationMiddlewares = [
|
||||||
Middleware\InitializeTenancyByDomain::class,
|
Middleware\InitializeTenancyByDomain::class,
|
||||||
Middleware\InitializeTenancyBySubdomain::class,
|
Middleware\InitializeTenancyBySubdomain::class,
|
||||||
|
|
@ -42,7 +43,10 @@ class UniversalRoutes implements Feature
|
||||||
|
|
||||||
public static function routeHasMiddleware(Route $route, string $middleware): bool
|
public static function routeHasMiddleware(Route $route, string $middleware): bool
|
||||||
{
|
{
|
||||||
if (in_array($middleware, $route->middleware(), true)) {
|
/** @var array $routeMiddleware */
|
||||||
|
$routeMiddleware = $route->middleware();
|
||||||
|
|
||||||
|
if (in_array($middleware, $routeMiddleware, true)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
28
src/Jobs/ClearPendingTenants.php
Normal file
28
src/Jobs/ClearPendingTenants.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 Illuminate\Support\Facades\Artisan;
|
||||||
|
use Stancl\Tenancy\Commands\ClearPendingTenants as ClearPendingTenantsCommand;
|
||||||
|
|
||||||
|
class ClearPendingTenants implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
Artisan::call(ClearPendingTenantsCommand::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ class CreateDatabase implements ShouldQueue
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(DatabaseManager $databaseManager)
|
public function handle(DatabaseManager $databaseManager): bool
|
||||||
{
|
{
|
||||||
event(new CreatingDatabase($this->tenant));
|
event(new CreatingDatabase($this->tenant));
|
||||||
|
|
||||||
|
|
@ -38,5 +38,7 @@ class CreateDatabase implements ShouldQueue
|
||||||
$this->tenant->database()->manager()->createDatabase($this->tenant);
|
$this->tenant->database()->manager()->createDatabase($this->tenant);
|
||||||
|
|
||||||
event(new DatabaseCreated($this->tenant));
|
event(new DatabaseCreated($this->tenant));
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/Jobs/CreatePendingTenants.php
Normal file
28
src/Jobs/CreatePendingTenants.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 Illuminate\Support\Facades\Artisan;
|
||||||
|
use Stancl\Tenancy\Commands\CreatePendingTenants as CreatePendingTenantsCommand;
|
||||||
|
|
||||||
|
class CreatePendingTenants implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
Artisan::call(CreatePendingTenantsCommand::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
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\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
|
|
||||||
class DeleteDomains
|
class DeleteDomains
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
/** @var TenantWithDatabase&Model&HasDomains */ // todo unresolvable type for phpstan
|
|
||||||
protected TenantWithDatabase&Model $tenant;
|
protected TenantWithDatabase&Model $tenant;
|
||||||
|
|
||||||
public function __construct(TenantWithDatabase&Model $tenant)
|
public function __construct(TenantWithDatabase&Model $tenant)
|
||||||
|
|
|
||||||
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;
|
namespace Stancl\Tenancy\Listeners;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Events\BootstrappingTenancy;
|
use Stancl\Tenancy\Events\BootstrappingTenancy;
|
||||||
use Stancl\Tenancy\Events\TenancyBootstrapped;
|
use Stancl\Tenancy\Events\TenancyBootstrapped;
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
|
@ -15,7 +16,10 @@ class BootstrapTenancy
|
||||||
event(new BootstrappingTenancy($event->tenancy));
|
event(new BootstrappingTenancy($event->tenancy));
|
||||||
|
|
||||||
foreach ($event->tenancy->getBootstrappers() as $bootstrapper) {
|
foreach ($event->tenancy->getBootstrappers() as $bootstrapper) {
|
||||||
$bootstrapper->bootstrap($event->tenancy->tenant);
|
/** @var Tenant $tenant */
|
||||||
|
$tenant = $event->tenancy->tenant;
|
||||||
|
|
||||||
|
$bootstrapper->bootstrap($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
event(new TenancyBootstrapped($event->tenancy));
|
event(new TenancyBootstrapped($event->tenancy));
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,22 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Listeners;
|
namespace Stancl\Tenancy\Listeners;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
use Stancl\Tenancy\Database\DatabaseManager;
|
use Stancl\Tenancy\Database\DatabaseManager;
|
||||||
use Stancl\Tenancy\Events\Contracts\TenantEvent;
|
use Stancl\Tenancy\Events\Contracts\TenantEvent;
|
||||||
|
|
||||||
class CreateTenantConnection
|
class CreateTenantConnection
|
||||||
{
|
{
|
||||||
/** @var DatabaseManager */
|
public function __construct(
|
||||||
protected $database;
|
protected DatabaseManager $database,
|
||||||
|
) {
|
||||||
public function __construct(DatabaseManager $database)
|
|
||||||
{
|
|
||||||
$this->database = $database;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(TenantEvent $event): void
|
public function handle(TenantEvent $event): void
|
||||||
{
|
{
|
||||||
$this->database->createTenantConnection($event->tenant);
|
/** @var TenantWithDatabase */
|
||||||
|
$tenant = $event->tenant;
|
||||||
|
|
||||||
|
$this->database->createTenantConnection($tenant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 static bool $shouldQueue = false;
|
||||||
|
|
||||||
public function shouldQueue($event): bool
|
public function shouldQueue(object $event): bool
|
||||||
{
|
{
|
||||||
if (static::$shouldQueue) {
|
if (static::$shouldQueue) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class RevertToCentralContext
|
||||||
{
|
{
|
||||||
event(new RevertingToCentralContext($event->tenancy));
|
event(new RevertingToCentralContext($event->tenancy));
|
||||||
|
|
||||||
foreach ($event->tenancy->getBootstrappers() as $bootstrapper) {
|
foreach (array_reverse($event->tenancy->getBootstrappers()) as $bootstrapper) {
|
||||||
$bootstrapper->revert();
|
$bootstrapper->revert();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,19 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Listeners;
|
namespace Stancl\Tenancy\Listeners;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Stancl\Tenancy\Contracts\Syncable;
|
||||||
use Stancl\Tenancy\Contracts\SyncMaster;
|
use Stancl\Tenancy\Contracts\SyncMaster;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
use Stancl\Tenancy\Database\TenantCollection;
|
||||||
use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase;
|
use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase;
|
||||||
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||||
use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException;
|
use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException;
|
||||||
|
|
||||||
|
// todo@v4 review all code related to resource syncing
|
||||||
|
|
||||||
class UpdateSyncedResource extends QueueableListener
|
class UpdateSyncedResource extends QueueableListener
|
||||||
{
|
{
|
||||||
public static bool $shouldQueue = false;
|
public static bool $shouldQueue = false;
|
||||||
|
|
@ -30,25 +35,28 @@ class UpdateSyncedResource extends QueueableListener
|
||||||
$this->updateResourceInTenantDatabases($tenants, $event, $syncedAttributes);
|
$this->updateResourceInTenantDatabases($tenants, $event, $syncedAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTenantsForCentralModel($centralModel): EloquentCollection
|
protected function getTenantsForCentralModel(Syncable $centralModel): TenantCollection
|
||||||
{
|
{
|
||||||
if (! $centralModel instanceof SyncMaster) {
|
if (! $centralModel instanceof SyncMaster) {
|
||||||
// If we're trying to use a tenant User model instead of the central User model, for example.
|
// If we're trying to use a tenant User model instead of the central User model, for example.
|
||||||
throw new ModelNotSyncMasterException(get_class($centralModel));
|
throw new ModelNotSyncMasterException(get_class($centralModel));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var SyncMaster|Model $centralModel */
|
/** @var Tenant&Model&SyncMaster $centralModel */
|
||||||
|
|
||||||
// Since this model is "dirty" (taken by reference from the event), it might have the tenants
|
// Since this model is "dirty" (taken by reference from the event), it might have the tenants
|
||||||
// relationship already loaded and cached. For this reason, we refresh the relationship.
|
// relationship already loaded and cached. For this reason, we refresh the relationship.
|
||||||
$centralModel->load('tenants');
|
$centralModel->load('tenants');
|
||||||
|
|
||||||
return $centralModel->tenants;
|
/** @var TenantCollection $tenants */
|
||||||
|
$tenants = $centralModel->tenants;
|
||||||
|
|
||||||
|
return $tenants;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function updateResourceInCentralDatabaseAndGetTenants($event, $syncedAttributes): EloquentCollection
|
protected function updateResourceInCentralDatabaseAndGetTenants(SyncedResourceSaved $event, array $syncedAttributes): TenantCollection
|
||||||
{
|
{
|
||||||
/** @var Model|SyncMaster $centralModel */
|
/** @var (Model&SyncMaster)|null $centralModel */
|
||||||
$centralModel = $event->model->getCentralModelName()::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey())
|
$centralModel = $event->model->getCentralModelName()::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey())
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
|
@ -59,15 +67,17 @@ class UpdateSyncedResource extends QueueableListener
|
||||||
event(new SyncedResourceChangedInForeignDatabase($event->model, null));
|
event(new SyncedResourceChangedInForeignDatabase($event->model, null));
|
||||||
} else {
|
} else {
|
||||||
// If the resource doesn't exist at all in the central DB,we create
|
// If the resource doesn't exist at all in the central DB,we create
|
||||||
// the record with all attributes, not just the synced ones.
|
$centralModel = $event->model->getCentralModelName()::create($this->getAttributesForCreation($event->model));
|
||||||
$centralModel = $event->model->getCentralModelName()::create($event->model->getAttributes());
|
|
||||||
event(new SyncedResourceChangedInForeignDatabase($event->model, null));
|
event(new SyncedResourceChangedInForeignDatabase($event->model, null));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the model was just created, the mapping of the tenant to the user likely doesn't exist, so we create it.
|
// If the model was just created, the mapping of the tenant to the user likely doesn't exist, so we create it.
|
||||||
$currentTenantMapping = function ($model) use ($event) {
|
$currentTenantMapping = function ($model) use ($event) {
|
||||||
return ((string) $model->pivot->tenant_id) === ((string) $event->tenant->getTenantKey());
|
/** @var Tenant */
|
||||||
|
$tenant = $event->tenant;
|
||||||
|
|
||||||
|
return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey());
|
||||||
};
|
};
|
||||||
|
|
||||||
$mappingExists = $centralModel->tenants->contains($currentTenantMapping);
|
$mappingExists = $centralModel->tenants->contains($currentTenantMapping);
|
||||||
|
|
@ -76,22 +86,29 @@ class UpdateSyncedResource extends QueueableListener
|
||||||
// Here we should call TenantPivot, but we call general Pivot, so that this works
|
// Here we should call TenantPivot, but we call general Pivot, so that this works
|
||||||
// even if people use their own pivot model that is not based on our TenantPivot
|
// even if people use their own pivot model that is not based on our TenantPivot
|
||||||
Pivot::withoutEvents(function () use ($centralModel, $event) {
|
Pivot::withoutEvents(function () use ($centralModel, $event) {
|
||||||
$centralModel->tenants()->attach($event->tenant->getTenantKey());
|
/** @var Tenant */
|
||||||
|
$tenant = $event->tenant;
|
||||||
|
|
||||||
|
$centralModel->tenants()->attach($tenant->getTenantKey());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) {
|
/** @var TenantCollection $tenants */
|
||||||
|
$tenants = $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) {
|
||||||
// Remove the mapping for the current tenant.
|
// Remove the mapping for the current tenant.
|
||||||
return ! $currentTenantMapping($model);
|
return ! $currentTenantMapping($model);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return $tenants;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function updateResourceInTenantDatabases($tenants, $event, $syncedAttributes): void
|
protected function updateResourceInTenantDatabases(TenantCollection $tenants, SyncedResourceSaved $event, array $syncedAttributes): void
|
||||||
{
|
{
|
||||||
tenancy()->runForMultiple($tenants, function ($tenant) use ($event, $syncedAttributes) {
|
tenancy()->runForMultiple($tenants, function ($tenant) use ($event, $syncedAttributes) {
|
||||||
// Forget instance state and find the model,
|
// Forget instance state and find the model,
|
||||||
// again in the current tenant's context.
|
// again in the current tenant's context.
|
||||||
|
|
||||||
|
/** @var Model&Syncable $eventModel */
|
||||||
$eventModel = $event->model;
|
$eventModel = $event->model;
|
||||||
|
|
||||||
if ($eventModel instanceof SyncMaster) {
|
if ($eventModel instanceof SyncMaster) {
|
||||||
|
|
@ -112,12 +129,53 @@ class UpdateSyncedResource extends QueueableListener
|
||||||
if ($localModel) {
|
if ($localModel) {
|
||||||
$localModel->update($syncedAttributes);
|
$localModel->update($syncedAttributes);
|
||||||
} else {
|
} else {
|
||||||
// When creating, we use all columns, not just the synced ones.
|
$localModel = $localModelClass::create($this->getAttributesForCreation($eventModel));
|
||||||
$localModel = $localModelClass::create($eventModel->getAttributes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
event(new SyncedResourceChangedInForeignDatabase($localModel, $tenant));
|
event(new SyncedResourceChangedInForeignDatabase($localModel, $tenant));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getAttributesForCreation(Model&Syncable $model): array
|
||||||
|
{
|
||||||
|
if (! $model->getSyncedCreationAttributes()) {
|
||||||
|
// Creation attributes are not specified so create the model as 1:1 copy
|
||||||
|
// exclude the "primary key" because we want primary key to handle by the target model to avoid duplication errors
|
||||||
|
$attributes = $model->getAttributes();
|
||||||
|
unset($attributes[$model->getKeyName()]);
|
||||||
|
|
||||||
|
return $attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Arr::isAssoc($model->getSyncedCreationAttributes())) {
|
||||||
|
// Developer provided the default values (key => value) or mix of default values and attribute names (values only)
|
||||||
|
// We will merge the default values with provided attributes and sync attributes
|
||||||
|
[$attributeNames, $defaultValues] = $this->getAttributeNamesAndDefaultValues($model);
|
||||||
|
$attributes = $model->only(array_merge($model->getSyncedAttributeNames(), $attributeNames));
|
||||||
|
|
||||||
|
return array_merge($attributes, $defaultValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Developer provided the attribute names, so we'll use them to pick model attributes
|
||||||
|
return $model->only($model->getSyncedCreationAttributes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split the attribute names (sequential index items) and default values (key => values).
|
||||||
|
*/
|
||||||
|
protected function getAttributeNamesAndDefaultValues(Model&Syncable $model): array
|
||||||
|
{
|
||||||
|
$syncedCreationAttributes = $model->getSyncedCreationAttributes() ?? [];
|
||||||
|
|
||||||
|
$attributes = Arr::where($syncedCreationAttributes, function ($value, $key) {
|
||||||
|
return is_numeric($key);
|
||||||
|
});
|
||||||
|
|
||||||
|
$defaultValues = Arr::where($syncedCreationAttributes, function ($value, $key) {
|
||||||
|
return is_string($key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [$attributes, $defaultValues];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ namespace Stancl\Tenancy\Middleware;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
||||||
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
||||||
use Symfony\Component\HttpFoundation\IpUtils;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
|
class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
|
||||||
|
|
@ -21,19 +20,38 @@ class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
|
||||||
if (tenant('maintenance_mode')) {
|
if (tenant('maintenance_mode')) {
|
||||||
$data = tenant('maintenance_mode');
|
$data = tenant('maintenance_mode');
|
||||||
|
|
||||||
if (isset($data['allowed']) && IpUtils::checkIp($request->ip(), (array) $data['allowed'])) {
|
if (isset($data['secret']) && $request->path() === $data['secret']) {
|
||||||
|
return $this->bypassResponse($data['secret']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasValidBypassCookie($request, $data) ||
|
||||||
|
$this->inExceptArray($request)) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->inExceptArray($request)) {
|
if (isset($data['redirect'])) {
|
||||||
return $next($request);
|
$path = $data['redirect'] === '/'
|
||||||
|
? $data['redirect']
|
||||||
|
: trim($data['redirect'], '/');
|
||||||
|
|
||||||
|
if ($request->path() !== $path) {
|
||||||
|
return redirect($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['template'])) {
|
||||||
|
return response(
|
||||||
|
$data['template'],
|
||||||
|
(int) ($data['status'] ?? 503),
|
||||||
|
$this->getHeaders($data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
503,
|
(int) ($data['status'] ?? 503),
|
||||||
'Service Unavailable',
|
'Service Unavailable',
|
||||||
null,
|
null,
|
||||||
isset($data['retry']) ? ['Retry-After' => $data['retry']] : []
|
$this->getHeaders($data)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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