mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 02:54:03 +00:00
Merge branch 'master' into configurable-force-rls
This commit is contained in:
commit
f9f9e1814a
92 changed files with 1056 additions and 497 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- laravel: "^11.0"
|
- laravel: "^12.0"
|
||||||
php: "8.4"
|
php: "8.4"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
18
.github/workflows/queue.yml
vendored
18
.github/workflows/queue.yml
vendored
|
|
@ -10,11 +10,19 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare composer version constraint prefix
|
- name: Prepare composer version constraint prefix
|
||||||
run: |
|
run: |
|
||||||
BRANCH=${GITHUB_REF#refs/heads/}
|
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
if [[ $BRANCH =~ ^[0-9] ]]; then
|
# For refs like "refs/tags/v3.9.0", remove "refs/tags/v" prefix to get just "3.9.0"
|
||||||
echo "VERSION_PREFIX=${BRANCH}-dev" >> $GITHUB_ENV
|
VERSION=${GITHUB_REF#refs/tags/v}
|
||||||
|
echo "VERSION_PREFIX=${VERSION}" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "VERSION_PREFIX=dev-${BRANCH}" >> $GITHUB_ENV
|
BRANCH=${GITHUB_REF#refs/heads/}
|
||||||
|
if [[ $BRANCH =~ ^[0-9]\.x$ ]]; then
|
||||||
|
# Branches starting with %d.x need to use -dev suffix
|
||||||
|
echo "VERSION_PREFIX=${BRANCH}-dev" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
# All other branches use dev-${branch} prefix
|
||||||
|
echo "VERSION_PREFIX=dev-${BRANCH}" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Clone test suite
|
- name: Clone test suite
|
||||||
|
|
@ -25,3 +33,5 @@ jobs:
|
||||||
cd tenancy-queue-tester
|
cd tenancy-queue-tester
|
||||||
TENANCY_VERSION=${VERSION_PREFIX}#${GITHUB_SHA} ./setup.sh
|
TENANCY_VERSION=${VERSION_PREFIX}#${GITHUB_SHA} ./setup.sh
|
||||||
./test.sh
|
./test.sh
|
||||||
|
./alternative_config.sh
|
||||||
|
PERSISTENT=1 ./test.sh
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,12 @@ To fix this, simply delete the database memory by shutting down containers and s
|
||||||
|
|
||||||
Same thing for `SQLSTATE[HY000]: General error: 1615 Prepared statement needs to be re-prepared`.
|
Same thing for `SQLSTATE[HY000]: General error: 1615 Prepared statement needs to be re-prepared`.
|
||||||
|
|
||||||
### Docker on M1
|
### Docker on Apple Silicon
|
||||||
|
|
||||||
Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1.
|
Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1.
|
||||||
|
|
||||||
|
2025 note: By now only MSSQL doesn't have good M1 support. The override also started being a bit problematic, having issues with starts, often requiring multiple starts. This often makes the original image in docker-compose more stable, even if it's amd64-only. With Rosetta enabled, you should be able to use it without issues.
|
||||||
|
|
||||||
### Coverage reports
|
### Coverage reports
|
||||||
|
|
||||||
To run tests and generate coverage reports, use `composer test-full`.
|
To run tests and generate coverage reports, use `composer test-full`.
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
Events\DeletingTenant::class => [
|
Events\DeletingTenant::class => [
|
||||||
JobPipeline::make([
|
JobPipeline::make([
|
||||||
Jobs\DeleteDomains::class,
|
Jobs\DeleteDomains::class,
|
||||||
|
// Jobs\RemoveStorageSymlinks::class,
|
||||||
])->send(function (Events\DeletingTenant $event) {
|
])->send(function (Events\DeletingTenant $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->shouldBeQueued(false),
|
})->shouldBeQueued(false),
|
||||||
|
|
@ -62,7 +63,6 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
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.
|
||||||
|
|
@ -145,24 +145,22 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
protected function overrideUrlInTenantContext(): void
|
protected function overrideUrlInTenantContext(): void
|
||||||
{
|
{
|
||||||
/**
|
// \Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant, string $originalRootUrl) {
|
||||||
* Import your tenant model!
|
// $tenantDomain = $tenant instanceof \Stancl\Tenancy\Contracts\SingleDomainTenant
|
||||||
*
|
// ? $tenant->domain
|
||||||
* \Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant, string $originalRootUrl) {
|
// : $tenant->domains->first()->domain;
|
||||||
* $tenantDomain = $tenant instanceof \Stancl\Tenancy\Contracts\SingleDomainTenant
|
//
|
||||||
* ? $tenant->domain
|
// $scheme = str($originalRootUrl)->before('://');
|
||||||
* : $tenant->domains->first()->domain;
|
//
|
||||||
*
|
// if (str_contains($tenantDomain, '.')) {
|
||||||
* $scheme = str($originalRootUrl)->before('://');
|
// // Domain identification
|
||||||
*
|
// return $scheme . '://' . $tenantDomain . '/';
|
||||||
* // If you're using domain identification:
|
// } else {
|
||||||
* return $scheme . '://' . $tenantDomain . '/';
|
// // Subdomain identification
|
||||||
*
|
// $originalDomain = str($originalRootUrl)->after($scheme . '://')->before('/');
|
||||||
* // If you're using subdomain identification:
|
// return $scheme . '://' . $tenantDomain . '.' . $originalDomain . '/';
|
||||||
* $originalDomain = str($originalRootUrl)->after($scheme . '://');
|
// }
|
||||||
* return $scheme . '://' . $tenantDomain . '.' . $originalDomain . '/';
|
// };
|
||||||
* };
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function register()
|
public function register()
|
||||||
|
|
@ -178,32 +176,17 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
$this->makeTenancyMiddlewareHighestPriority();
|
$this->makeTenancyMiddlewareHighestPriority();
|
||||||
$this->overrideUrlInTenantContext();
|
$this->overrideUrlInTenantContext();
|
||||||
|
|
||||||
/**
|
// // Include soft deleted resources in synced resource queries.
|
||||||
* Include soft deleted resources in synced resource queries.
|
// ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
|
||||||
*
|
// if ($query->hasMacro('withTrashed')) {
|
||||||
* ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
|
// $query->withTrashed();
|
||||||
* if ($query->hasMacro('withTrashed')) {
|
// }
|
||||||
* $query->withTrashed();
|
// };
|
||||||
* }
|
|
||||||
* };
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
// // To make Livewire v3 work with Tenancy, make the update route universal.
|
||||||
* To make Livewire v3 work with Tenancy, make the update route universal.
|
// Livewire::setUpdateRoute(function ($handle) {
|
||||||
*
|
// return RouteFacade::post('/livewire/update', $handle)->middleware(['web', 'universal', \Stancl\Tenancy\Tenancy::defaultMiddleware()]);
|
||||||
* Livewire::setUpdateRoute(function ($handle) {
|
// });
|
||||||
* return RouteFacade::post('/livewire/update', $handle)->middleware(['web', 'universal']);
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
// if (InitializeTenancyByRequestData::inGlobalStack()) {
|
|
||||||
// FortifyRouteBootstrapper::$fortifyHome = 'dashboard';
|
|
||||||
// TenancyUrlGenerator::$prefixRouteNames = false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (tenancy()->globalStackHasMiddleware(config('tenancy.identification.path_identification_middleware'))) {
|
|
||||||
TenancyUrlGenerator::$prefixRouteNames = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function bootEvents()
|
protected function bootEvents()
|
||||||
|
|
@ -228,10 +211,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
->group(base_path('routes/tenant.php'));
|
->group(base_path('routes/tenant.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete this condition when using route-level path identification
|
// $this->cloneRoutes();
|
||||||
if (tenancy()->globalStackHasMiddleware(config('tenancy.identification.path_identification_middleware'))) {
|
|
||||||
$this->cloneRoutes();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,16 +225,13 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
/** @var CloneRoutesAsTenant $cloneRoutes */
|
/** @var CloneRoutesAsTenant $cloneRoutes */
|
||||||
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);
|
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);
|
||||||
|
|
||||||
/**
|
// // You can provide a closure for cloning a specific route, e.g.:
|
||||||
* You can provide a closure for cloning a specific route, e.g.:
|
// $cloneRoutes->cloneUsing('welcome', function () {
|
||||||
* $cloneRoutes->cloneUsing('welcome', function () {
|
// RouteFacade::get('/tenant-welcome', fn () => 'Current tenant: ' . tenant()->getTenantKey())
|
||||||
* RouteFacade::get('/tenant-welcome', fn () => 'Current tenant: ' . tenant()->getTenantKey())
|
// ->middleware(['universal', InitializeTenancyByPath::class])
|
||||||
* ->middleware(['universal', InitializeTenancyByPath::class])
|
// ->name('tenant.welcome');
|
||||||
* ->name('tenant.welcome');
|
// });
|
||||||
* });
|
// // To see the default behavior of cloning the universal routes, check out the cloneRoute() method in CloneRoutesAsTenant.
|
||||||
*
|
|
||||||
* To see the default behavior of cloning the universal routes, check out the cloneRoute() method in CloneRoutesAsTenant.
|
|
||||||
*/
|
|
||||||
|
|
||||||
$cloneRoutes->handle();
|
$cloneRoutes->handle();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ return [
|
||||||
'models' => [
|
'models' => [
|
||||||
'tenant' => Stancl\Tenancy\Database\Models\Tenant::class,
|
'tenant' => Stancl\Tenancy\Database\Models\Tenant::class,
|
||||||
'domain' => Stancl\Tenancy\Database\Models\Domain::class,
|
'domain' => Stancl\Tenancy\Database\Models\Domain::class,
|
||||||
|
'impersonation_token' => Stancl\Tenancy\Database\Models\ImpersonationToken::class,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the column used to relate models to tenants.
|
* Name of the column used to relate models to tenants.
|
||||||
|
|
@ -33,6 +34,7 @@ return [
|
||||||
*
|
*
|
||||||
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator
|
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator
|
||||||
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator
|
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator
|
||||||
|
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomIntGenerator
|
||||||
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator
|
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator
|
||||||
*/
|
*/
|
||||||
'id_generator' => UniqueIdentifierGenerators\UUIDGenerator::class,
|
'id_generator' => UniqueIdentifierGenerators\UUIDGenerator::class,
|
||||||
|
|
@ -90,7 +92,7 @@ return [
|
||||||
/**
|
/**
|
||||||
* Identification middleware tenancy recognizes as path identification middleware.
|
* Identification middleware tenancy recognizes as path identification middleware.
|
||||||
*
|
*
|
||||||
* This is used during determining whether whether a path identification is used
|
* This is used for determining if a path identification middleware is used
|
||||||
* during operations specific to path identification, e.g. forgetting the tenant parameter in ForgetTenantParameter.
|
* during operations specific to path identification, e.g. forgetting the tenant parameter in ForgetTenantParameter.
|
||||||
*
|
*
|
||||||
* If you're using a custom path identification middleware, add it here.
|
* If you're using a custom path identification middleware, add it here.
|
||||||
|
|
@ -117,6 +119,7 @@ return [
|
||||||
Resolvers\PathTenantResolver::class => [
|
Resolvers\PathTenantResolver::class => [
|
||||||
'tenant_parameter_name' => 'tenant',
|
'tenant_parameter_name' => 'tenant',
|
||||||
'tenant_model_column' => null, // null = tenant key
|
'tenant_model_column' => null, // null = tenant key
|
||||||
|
'tenant_route_name_prefix' => null, // null = 'tenant.'
|
||||||
'allowed_extra_model_columns' => [], // used with binding route fields
|
'allowed_extra_model_columns' => [], // used with binding route fields
|
||||||
|
|
||||||
'cache' => false,
|
'cache' => false,
|
||||||
|
|
@ -129,8 +132,6 @@ return [
|
||||||
'cache_store' => null, // null = default
|
'cache_store' => null, // null = default
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
// todo@docs update integration guides to use Stancl\Tenancy::defaultMiddleware()
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -214,7 +215,14 @@ return [
|
||||||
// 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager::class, // Also permission controlled
|
// 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager::class, // Also permission controlled
|
||||||
],
|
],
|
||||||
|
|
||||||
// todo@docblock
|
/*
|
||||||
|
* Drop tenant databases when `php artisan migrate:fresh` is used.
|
||||||
|
* You may want to use this locally since deleting tenants only
|
||||||
|
* deletes their databases when they're deleted individually, not
|
||||||
|
* when the records are mass deleted from the database.
|
||||||
|
*
|
||||||
|
* Note: This overrides the default MigrateFresh command.
|
||||||
|
*/
|
||||||
'drop_tenant_databases_on_migrate_fresh' => false,
|
'drop_tenant_databases_on_migrate_fresh' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -319,7 +327,6 @@ return [
|
||||||
*/
|
*/
|
||||||
'url_override' => [
|
'url_override' => [
|
||||||
// Note that the local disk you add must exist in the tenancy.filesystem.root_override config
|
// Note that the local disk you add must exist in the tenancy.filesystem.root_override config
|
||||||
// todo@v4 Rename url_override to something that describes the config key better
|
|
||||||
'public' => 'public-%tenant%',
|
'public' => 'public-%tenant%',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -355,7 +362,7 @@ return [
|
||||||
* leave asset() helper tenancy disabled and explicitly use tenant_asset() calls in places
|
* leave asset() helper tenancy disabled and explicitly use tenant_asset() calls in places
|
||||||
* where you want to use tenant-specific assets (product images, avatars, etc).
|
* where you want to use tenant-specific assets (product images, avatars, etc).
|
||||||
*/
|
*/
|
||||||
'asset_helper_tenancy' => false, // todo@rename asset_helper_override?
|
'asset_helper_override' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,24 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4",
|
"php": "^8.4",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"illuminate/support": "^10.1|^11.3",
|
"illuminate/support": "^12.0",
|
||||||
"laravel/tinker": "^2.0",
|
"laravel/tinker": "^2.0",
|
||||||
"facade/ignition-contracts": "^1.0.2",
|
"facade/ignition-contracts": "^1.0.2",
|
||||||
"spatie/ignition": "^1.4",
|
"spatie/ignition": "^1.4",
|
||||||
"ramsey/uuid": "^4.7.3",
|
"ramsey/uuid": "^4.7.3",
|
||||||
"stancl/jobpipeline": "2.0.0-rc2",
|
"stancl/jobpipeline": "2.0.0-rc5",
|
||||||
"stancl/virtualcolumn": "dev-master",
|
"stancl/virtualcolumn": "^1.5.0",
|
||||||
"spatie/invade": "^1.1",
|
"spatie/invade": "*",
|
||||||
"laravel/prompts": "0.*"
|
"laravel/prompts": "0.*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/framework": "^10.1|^11.3",
|
"laravel/framework": "^12.0",
|
||||||
"orchestra/testbench": "^8.0|^9.0",
|
"orchestra/testbench": "^10.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.12.2",
|
"league/flysystem-aws-s3-v3": "^3.12.2",
|
||||||
"doctrine/dbal": "^3.6.0",
|
"doctrine/dbal": "^3.6.0",
|
||||||
"spatie/valuestore": "^1.2.5",
|
"spatie/valuestore": "^1.2.5",
|
||||||
"pestphp/pest": "^2.0",
|
"pestphp/pest": "^3.0",
|
||||||
"larastan/larastan": "^3.0",
|
"larastan/larastan": "^3.0"
|
||||||
"spatie/invade": "^1.1",
|
|
||||||
"aws/aws-sdk-php-laravel": "~3.0"
|
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
services:
|
services:
|
||||||
mysql:
|
|
||||||
# platform: linux/amd64 # either one works
|
|
||||||
image: arm64v8/mysql
|
|
||||||
mysql2:
|
|
||||||
# platform: linux/amd64 # either one works
|
|
||||||
image: arm64v8/mysql
|
|
||||||
mssql:
|
mssql:
|
||||||
image: mcr.microsoft.com/azure-sql-edge
|
image: mcr.microsoft.com/azure-sql-edge
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ services:
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:5.7
|
image: mysql:8
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: password
|
MYSQL_ROOT_PASSWORD: password
|
||||||
MYSQL_DATABASE: main
|
MYSQL_DATABASE: main
|
||||||
|
|
@ -46,7 +46,7 @@ services:
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /var/lib/mysql
|
- /var/lib/mysql
|
||||||
mysql2:
|
mysql2:
|
||||||
image: mysql:5.7
|
image: mysql:8
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: password
|
MYSQL_ROOT_PASSWORD: password
|
||||||
MYSQL_DATABASE: main
|
MYSQL_DATABASE: main
|
||||||
|
|
@ -72,12 +72,12 @@ services:
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /var/lib/postgresql/data
|
- /var/lib/postgresql/data
|
||||||
mssql:
|
mssql:
|
||||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||||
environment:
|
environment:
|
||||||
- ACCEPT_EULA=Y
|
- ACCEPT_EULA=Y
|
||||||
- SA_PASSWORD=P@ssword # todo reuse env from above
|
- SA_PASSWORD=P@ssword # todo reuse env from above
|
||||||
healthcheck: # https://github.com/Microsoft/mssql-docker/issues/133#issuecomment-1995615432
|
healthcheck: # https://github.com/Microsoft/mssql-docker/issues/133#issuecomment-1995615432
|
||||||
test: timeout 1 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/1433'
|
test: timeout 2 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/1433'
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
includes:
|
includes:
|
||||||
- ./vendor/larastan/larastan/extension.neon
|
- ./vendor/larastan/larastan/extension.neon
|
||||||
- ./vendor/spatie/invade/phpstan-extension.neon
|
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
paths:
|
paths:
|
||||||
|
|
@ -16,6 +15,12 @@ parameters:
|
||||||
ignoreErrors:
|
ignoreErrors:
|
||||||
- identifier: trait.unused
|
- identifier: trait.unused
|
||||||
- identifier: missingType.iterableValue
|
- identifier: missingType.iterableValue
|
||||||
|
-
|
||||||
|
message: '#Spatie\\Invade\\Invader#'
|
||||||
|
identifier: method.notFound
|
||||||
|
-
|
||||||
|
message: '#Spatie\\Invade\\Invader#'
|
||||||
|
identifier: property.notFound
|
||||||
- '#FFI#'
|
- '#FFI#'
|
||||||
- '#Return type(.*?) of method Stancl\\Tenancy\\Database\\Models\\Tenant\:\:newCollection\(\) should be compatible with return type#'
|
- '#Return type(.*?) of method Stancl\\Tenancy\\Database\\Models\\Tenant\:\:newCollection\(\) should be compatible with return type#'
|
||||||
- '#Method Stancl\\Tenancy\\Database\\Models\\Tenant\:\:newCollection\(\) should return#'
|
- '#Method Stancl\\Tenancy\\Database\\Models\\Tenant\:\:newCollection\(\) should return#'
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
|
||||||
protected function assetHelper(string|false $suffix): void
|
protected function assetHelper(string|false $suffix): void
|
||||||
{
|
{
|
||||||
if (! $this->app['config']['tenancy.filesystem.asset_helper_tenancy']) {
|
if (! $this->app['config']['tenancy.filesystem.asset_helper_override']) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,54 +7,52 @@ namespace Stancl\Tenancy\Bootstrappers\Integrations;
|
||||||
use Illuminate\Config\Repository;
|
use Illuminate\Config\Repository;
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Enums\Context;
|
|
||||||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows customizing Fortify action redirects
|
* Allows customizing Fortify action redirects so that they can also redirect
|
||||||
* so that they can also redirect to tenant routes instead of just the central routes.
|
* to tenant routes instead of just the central routes.
|
||||||
*
|
*
|
||||||
* Works with path and query string identification.
|
* This should be used with path/query string identification OR when using Fortify
|
||||||
|
* universally, including with domains.
|
||||||
|
*
|
||||||
|
* When using domain identification, there's no need to pass the tenant parameter,
|
||||||
|
* you only want to customize the routes being used, so you can set $passTenantParameter
|
||||||
|
* to false.
|
||||||
*/
|
*/
|
||||||
class FortifyRouteBootstrapper implements TenancyBootstrapper
|
class FortifyRouteBootstrapper implements TenancyBootstrapper
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Make Fortify actions redirect to custom routes.
|
* Fortify redirects that should be used in tenant context.
|
||||||
*
|
*
|
||||||
* For each route redirect, specify the intended route context (central or tenant).
|
* Syntax: ['redirect_name' => 'tenant_route_name']
|
||||||
* Based on the provided context, we pass the tenant parameter to the route (or not).
|
|
||||||
* The tenant parameter is only passed to the route when you specify its context as tenant.
|
|
||||||
*
|
|
||||||
* The route redirects should be in the following format:
|
|
||||||
*
|
|
||||||
* 'fortify_action' => [
|
|
||||||
* 'route_name' => 'tenant.route',
|
|
||||||
* 'context' => Context::TENANT,
|
|
||||||
* ]
|
|
||||||
*
|
|
||||||
* For example:
|
|
||||||
*
|
|
||||||
* FortifyRouteBootstrapper::$fortifyRedirectMap = [
|
|
||||||
* // On logout, redirect the user to the "bye" route in the central app
|
|
||||||
* 'logout' => [
|
|
||||||
* 'route_name' => 'bye',
|
|
||||||
* 'context' => Context::CENTRAL,
|
|
||||||
* ],
|
|
||||||
*
|
|
||||||
* // On login, redirect the user to the "welcome" route in the tenant app
|
|
||||||
* 'login' => [
|
|
||||||
* 'route_name' => 'welcome',
|
|
||||||
* 'context' => Context::TENANT,
|
|
||||||
* ],
|
|
||||||
* ];
|
|
||||||
*/
|
*/
|
||||||
public static array $fortifyRedirectMap = [];
|
public static array $fortifyRedirectMap = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the tenant parameter be passed to fortify routes in the tenant context.
|
||||||
|
*
|
||||||
|
* This should be enabled with path/query string identification and disabled with domain identification.
|
||||||
|
*
|
||||||
|
* You may also disable this when using path/query string identification if passing the tenant parameter
|
||||||
|
* is handled in another way (TenancyUrlGenerator::$passTenantParameter for both,
|
||||||
|
* UrlGeneratorBootstrapper:$addTenantParameterToDefaults for path identification).
|
||||||
|
*/
|
||||||
|
public static bool $passTenantParameter = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenant route that serves as Fortify's home (e.g. a tenant dashboard route).
|
* Tenant route that serves as Fortify's home (e.g. a tenant dashboard route).
|
||||||
* This route will always receive the tenant parameter.
|
* This route will always receive the tenant parameter.
|
||||||
*/
|
*/
|
||||||
public static string $fortifyHome = 'tenant.dashboard';
|
public static string|null $fortifyHome = 'tenant.dashboard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use default parameter names ('tenant' name and tenant key value) instead of the parameter name
|
||||||
|
* and column name configured in the path resolver config.
|
||||||
|
*
|
||||||
|
* You want to enable this when using query string identification while having customized that config.
|
||||||
|
*/
|
||||||
|
public static bool $defaultParameterNames = false;
|
||||||
|
|
||||||
protected array $originalFortifyConfig = [];
|
protected array $originalFortifyConfig = [];
|
||||||
|
|
||||||
|
|
@ -76,27 +74,22 @@ class FortifyRouteBootstrapper implements TenancyBootstrapper
|
||||||
|
|
||||||
protected function useTenantRoutesInFortify(Tenant $tenant): void
|
protected function useTenantRoutesInFortify(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$tenantKey = $tenant->getTenantKey();
|
$tenantParameterName = static::$defaultParameterNames ? 'tenant' : PathTenantResolver::tenantParameterName();
|
||||||
$tenantParameterName = PathTenantResolver::tenantParameterName();
|
$tenantParameterValue = static::$defaultParameterNames ? $tenant->getTenantKey() : PathTenantResolver::tenantParameterValue($tenant);
|
||||||
|
|
||||||
$generateLink = function (array $redirect) use ($tenantKey, $tenantParameterName) {
|
$generateLink = function (string $redirect) use ($tenantParameterValue, $tenantParameterName) {
|
||||||
// Specifying the context is only required with query string identification
|
return route($redirect, static::$passTenantParameter ? [$tenantParameterName => $tenantParameterValue] : []);
|
||||||
// because with path identification, the tenant parameter should always present
|
|
||||||
$passTenantParameter = $redirect['context'] === Context::TENANT;
|
|
||||||
|
|
||||||
// Only pass the tenant parameter when the user should be redirected to a tenant route
|
|
||||||
return route($redirect['route_name'], $passTenantParameter ? [$tenantParameterName => $tenantKey] : []);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get redirect URLs for the configured redirect routes
|
// Get redirect URLs for the configured redirect routes
|
||||||
$redirects = array_merge(
|
$redirects = array_merge(
|
||||||
$this->originalFortifyConfig['redirects'] ?? [], // Fortify config redirects
|
$this->originalFortifyConfig['redirects'] ?? [], // Fortify config redirects
|
||||||
array_map(fn (array $redirect) => $generateLink($redirect), static::$fortifyRedirectMap), // Mapped redirects
|
array_map(fn (string $redirect) => $generateLink($redirect), static::$fortifyRedirectMap), // Mapped redirects
|
||||||
);
|
);
|
||||||
|
|
||||||
if (static::$fortifyHome) {
|
if (static::$fortifyHome) {
|
||||||
// Generate the home route URL with the tenant parameter and make it the Fortify home route
|
// Generate the home route URL with the tenant parameter and make it the Fortify home route
|
||||||
$this->config->set('fortify.home', route(static::$fortifyHome, [$tenantParameterName => $tenantKey]));
|
$this->config->set('fortify.home', route(static::$fortifyHome, static::$passTenantParameter ? [$tenantParameterName => $tenantParameterValue] : []));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->config->set('fortify.redirects', $redirects);
|
$this->config->set('fortify.redirects', $redirects);
|
||||||
|
|
|
||||||
146
src/Bootstrappers/PersistentQueueTenancyBootstrapper.php
Normal file
146
src/Bootstrappers/PersistentQueueTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Bootstrappers;
|
||||||
|
|
||||||
|
use Illuminate\Config\Repository;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
|
use Illuminate\Queue\Events\JobFailed;
|
||||||
|
use Illuminate\Queue\Events\JobProcessed;
|
||||||
|
use Illuminate\Queue\Events\JobProcessing;
|
||||||
|
use Illuminate\Queue\Events\JobRetryRequested;
|
||||||
|
use Illuminate\Queue\QueueManager;
|
||||||
|
use Illuminate\Support\Testing\Fakes\QueueFake;
|
||||||
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
class PersistentQueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
{
|
||||||
|
/** @var Repository */
|
||||||
|
protected $config;
|
||||||
|
|
||||||
|
/** @var QueueManager */
|
||||||
|
protected $queue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The normal constructor is only executed after tenancy is bootstrapped.
|
||||||
|
* However, we're registering a hook to initialize tenancy. Therefore,
|
||||||
|
* we need to register the hook at service provider execution time.
|
||||||
|
*/
|
||||||
|
public static function __constructStatic(Application $app): void
|
||||||
|
{
|
||||||
|
static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(Repository $config, QueueManager $queue)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->queue = $queue;
|
||||||
|
|
||||||
|
$this->setUpPayloadGenerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function setUpJobListener(Dispatcher $dispatcher, bool $runningTests): void
|
||||||
|
{
|
||||||
|
$previousTenant = null;
|
||||||
|
|
||||||
|
$dispatcher->listen(JobProcessing::class, function ($event) use (&$previousTenant) {
|
||||||
|
$previousTenant = tenant();
|
||||||
|
|
||||||
|
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
|
||||||
|
$previousTenant = tenant();
|
||||||
|
|
||||||
|
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
|
||||||
|
$revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) {
|
||||||
|
if ($runningTests) {
|
||||||
|
static::revertToPreviousState($event->job->payload()['tenant_id'] ?? null, $previousTenant);
|
||||||
|
|
||||||
|
// We don't need to reset $previousTenant since the value will be set again when a job is processed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not running tests, we remain in the tenant's context. This makes other JobProcessed
|
||||||
|
// listeners able to deserialize the job, including with SerializesModels, since the tenant connection
|
||||||
|
// remains open.
|
||||||
|
};
|
||||||
|
|
||||||
|
$dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds
|
||||||
|
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
|
||||||
|
{
|
||||||
|
if (! $tenantId) {
|
||||||
|
// The job is not tenant-aware
|
||||||
|
if (tenancy()->initialized) {
|
||||||
|
// Tenancy was initialized, so we revert back to the central context
|
||||||
|
tenancy()->end();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-initialize tenancy between all jobs even if the tenant is the same
|
||||||
|
// so that we don't work with an outdated tenant() instance in case it
|
||||||
|
// was updated outside the queue worker.
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
/** @var Tenant $tenant */
|
||||||
|
$tenant = tenancy()->find($tenantId);
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function revertToPreviousState(string|int|null $tenantId, ?Tenant $previousTenant): void
|
||||||
|
{
|
||||||
|
// The job was not tenant-aware
|
||||||
|
if (! $tenantId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert back to the previous tenant
|
||||||
|
if (tenant() && $previousTenant && $previousTenant->isNot(tenant())) {
|
||||||
|
tenancy()->initialize($previousTenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
// End tenancy
|
||||||
|
if (tenant() && (! $previousTenant)) {
|
||||||
|
tenancy()->end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUpPayloadGenerator(): void
|
||||||
|
{
|
||||||
|
$bootstrapper = &$this;
|
||||||
|
|
||||||
|
if (! $this->queue instanceof QueueFake) {
|
||||||
|
$this->queue->createPayloadUsing(function ($connection) use (&$bootstrapper) {
|
||||||
|
return $bootstrapper->getPayload($connection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayload(string $connection): array
|
||||||
|
{
|
||||||
|
if (! tenancy()->initialized) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->config["queue.connections.$connection.central"]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tenant_id' => tenant()->getTenantKey(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bootstrap(Tenant $tenant): void {}
|
||||||
|
public function revert(): void {}
|
||||||
|
}
|
||||||
|
|
@ -24,16 +24,6 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
/** @var QueueManager */
|
/** @var QueueManager */
|
||||||
protected $queue;
|
protected $queue;
|
||||||
|
|
||||||
/**
|
|
||||||
* Don't persist the same tenant across multiple jobs even if they have the same tenant ID.
|
|
||||||
*
|
|
||||||
* This is useful when you're changing the tenant's state (e.g. properties in the `data` column) and want the next job to initialize tenancy again
|
|
||||||
* with the new data. Features like the Tenant Config are only executed when tenancy is initialized, so the re-initialization is needed in some cases.
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
public static $forceRefresh = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The normal constructor is only executed after tenancy is bootstrapped.
|
* The normal constructor is only executed after tenancy is bootstrapped.
|
||||||
* However, we're registering a hook to initialize tenancy. Therefore,
|
* However, we're registering a hook to initialize tenancy. Therefore,
|
||||||
|
|
@ -68,9 +58,12 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||||
});
|
});
|
||||||
|
|
||||||
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
|
|
||||||
$revertToPreviousState = function ($event) use (&$previousTenant) {
|
$revertToPreviousState = function ($event) use (&$previousTenant) {
|
||||||
static::revertToPreviousState($event, $previousTenant);
|
// In queue worker context, this reverts to the central context.
|
||||||
|
// In dispatchSync context, this reverts to the previous tenant's context.
|
||||||
|
// There's no need to reset $previousTenant here since it's always first
|
||||||
|
// set in the above listeners and the app is reverted back to that context.
|
||||||
|
static::revertToPreviousState($event->job->payload()['tenant_id'] ?? null, $previousTenant);
|
||||||
};
|
};
|
||||||
|
|
||||||
$dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds
|
$dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds
|
||||||
|
|
@ -79,61 +72,25 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
|
||||||
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
|
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
|
||||||
{
|
{
|
||||||
if ($tenantId === null) {
|
if (! $tenantId) {
|
||||||
// The job is not tenant-aware
|
|
||||||
if (tenancy()->initialized) {
|
|
||||||
// Tenancy was initialized, so we revert back to the central context
|
|
||||||
tenancy()->end();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (static::$forceRefresh) {
|
|
||||||
// Re-initialize tenancy between all jobs
|
|
||||||
if (tenancy()->initialized) {
|
|
||||||
tenancy()->end();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var Tenant $tenant */
|
|
||||||
$tenant = tenancy()->find($tenantId);
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tenancy()->initialized) {
|
|
||||||
// Tenancy is already initialized
|
|
||||||
if (tenant()->getTenantKey() === $tenantId) {
|
|
||||||
// It's initialized for the same tenant (e.g. dispatchSync was used, or the previous job also ran for this tenant)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tenancy was either not initialized, or initialized for a different tenant.
|
|
||||||
// Therefore, we initialize it for the correct tenant.
|
|
||||||
|
|
||||||
/** @var Tenant $tenant */
|
/** @var Tenant $tenant */
|
||||||
$tenant = tenancy()->find($tenantId);
|
$tenant = tenancy()->find($tenantId);
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function revertToPreviousState(JobProcessed|JobFailed $event, ?Tenant &$previousTenant): void
|
protected static function revertToPreviousState(string|int|null $tenantId, ?Tenant $previousTenant): void
|
||||||
{
|
{
|
||||||
$tenantId = $event->job->payload()['tenant_id'] ?? null;
|
// The job was not tenant-aware so no context switch was done
|
||||||
|
|
||||||
// The job was not tenant-aware
|
|
||||||
if (! $tenantId) {
|
if (! $tenantId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revert back to the previous tenant
|
// End tenancy when there's no previous tenant
|
||||||
if (tenant() && $previousTenant?->isNot(tenant())) {
|
// (= when running in a queue worker, not dispatchSync)
|
||||||
tenancy()->initialize($previousTenant);
|
if (tenant() && ! $previousTenant) {
|
||||||
}
|
|
||||||
|
|
||||||
// End tenancy
|
|
||||||
if (tenant() && (! $previousTenant)) {
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,16 +106,6 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
public function revert(): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPayload(string $connection): array
|
public function getPayload(string $connection): array
|
||||||
{
|
{
|
||||||
if (! tenancy()->initialized) {
|
if (! tenancy()->initialized) {
|
||||||
|
|
@ -169,10 +116,11 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$id = tenant()->getTenantKey();
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tenant_id' => $id,
|
'tenant_id' => tenant()->getTenantKey(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function bootstrap(Tenant $tenant): void {}
|
||||||
|
public function revert(): void {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ namespace Stancl\Tenancy\Bootstrappers;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Config\Repository;
|
use Illuminate\Config\Repository;
|
||||||
use Illuminate\Contracts\Foundation\Application;
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
use Illuminate\Routing\UrlGenerator;
|
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
|
@ -36,28 +35,43 @@ class RootUrlBootstrapper implements TenancyBootstrapper
|
||||||
|
|
||||||
protected string|null $originalRootUrl = null;
|
protected string|null $originalRootUrl = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overriding the root url may cause issues in *some* tests, so you can disable
|
||||||
|
* the behavior by setting this property to false.
|
||||||
|
*/
|
||||||
|
public static bool $rootUrlOverrideInTests = true;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected UrlGenerator $urlGenerator,
|
|
||||||
protected Repository $config,
|
protected Repository $config,
|
||||||
protected Application $app,
|
protected Application $app,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant): void
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
if ($this->app->runningInConsole() && static::$rootUrlOverride) {
|
if (static::$rootUrlOverride === null) {
|
||||||
$this->originalRootUrl = $this->urlGenerator->to('/');
|
return;
|
||||||
|
|
||||||
$newRootUrl = (static::$rootUrlOverride)($tenant, $this->originalRootUrl);
|
|
||||||
|
|
||||||
$this->urlGenerator->forceRootUrl($newRootUrl);
|
|
||||||
$this->config->set('app.url', $newRootUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->app->runningInConsole()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->app->runningUnitTests() && ! static::$rootUrlOverrideInTests) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->originalRootUrl = $this->app['url']->to('/');
|
||||||
|
|
||||||
|
$newRootUrl = (static::$rootUrlOverride)($tenant, $this->originalRootUrl);
|
||||||
|
|
||||||
|
$this->app['url']->forceRootUrl($newRootUrl);
|
||||||
|
$this->config->set('app.url', $newRootUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revert(): void
|
public function revert(): void
|
||||||
{
|
{
|
||||||
if ($this->originalRootUrl) {
|
if ($this->originalRootUrl) {
|
||||||
$this->urlGenerator->forceRootUrl($this->originalRootUrl);
|
$this->app['url']->forceRootUrl($this->originalRootUrl);
|
||||||
$this->config->set('app.url', $this->originalRootUrl);
|
$this->config->set('app.url', $this->originalRootUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use Illuminate\Support\Facades\URL;
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||||
|
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes the app use TenancyUrlGenerator (instead of Illuminate\Routing\UrlGenerator) which:
|
* Makes the app use TenancyUrlGenerator (instead of Illuminate\Routing\UrlGenerator) which:
|
||||||
|
|
@ -19,10 +20,20 @@ use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||||
* Used with path and query string identification.
|
* Used with path and query string identification.
|
||||||
*
|
*
|
||||||
* @see TenancyUrlGenerator
|
* @see TenancyUrlGenerator
|
||||||
* @see \Stancl\Tenancy\Resolvers\PathTenantResolver
|
* @see PathTenantResolver
|
||||||
*/
|
*/
|
||||||
class UrlGeneratorBootstrapper implements TenancyBootstrapper
|
class UrlGeneratorBootstrapper implements TenancyBootstrapper
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Should the tenant route parameter get added to TenancyUrlGenerator::defaults().
|
||||||
|
*
|
||||||
|
* This is recommended when using path identification since defaults() generally has better support in integrations,
|
||||||
|
* namely Ziggy, compared to TenancyUrlGenerator::$passTenantParameterToRoutes.
|
||||||
|
*
|
||||||
|
* With query string identification, this has no effect since URL::defaults() only works for route paramaters.
|
||||||
|
*/
|
||||||
|
public static bool $addTenantParameterToDefaults = true;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Application $app,
|
protected Application $app,
|
||||||
protected UrlGenerator $originalUrlGenerator,
|
protected UrlGenerator $originalUrlGenerator,
|
||||||
|
|
@ -32,12 +43,12 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper
|
||||||
{
|
{
|
||||||
URL::clearResolvedInstances();
|
URL::clearResolvedInstances();
|
||||||
|
|
||||||
$this->useTenancyUrlGenerator();
|
$this->useTenancyUrlGenerator($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revert(): void
|
public function revert(): void
|
||||||
{
|
{
|
||||||
$this->app->bind('url', fn () => $this->originalUrlGenerator);
|
$this->app->extend('url', fn () => $this->originalUrlGenerator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,26 +56,36 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper
|
||||||
*
|
*
|
||||||
* @see \Illuminate\Routing\RoutingServiceProvider registerUrlGenerator()
|
* @see \Illuminate\Routing\RoutingServiceProvider registerUrlGenerator()
|
||||||
*/
|
*/
|
||||||
protected function useTenancyUrlGenerator(): void
|
protected function useTenancyUrlGenerator(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$this->app->extend('url', function (UrlGenerator $urlGenerator, Application $app) {
|
$newGenerator = new TenancyUrlGenerator(
|
||||||
$newGenerator = new TenancyUrlGenerator(
|
$this->app['router']->getRoutes(),
|
||||||
$app['router']->getRoutes(),
|
$this->originalUrlGenerator->getRequest(),
|
||||||
$urlGenerator->getRequest(),
|
$this->app['config']->get('app.asset_url'),
|
||||||
$app['config']->get('app.asset_url'),
|
);
|
||||||
|
|
||||||
|
$defaultParameters = $this->originalUrlGenerator->getDefaultParameters();
|
||||||
|
|
||||||
|
if (static::$addTenantParameterToDefaults) {
|
||||||
|
$defaultParameters = array_merge(
|
||||||
|
$defaultParameters,
|
||||||
|
[
|
||||||
|
PathTenantResolver::tenantParameterName() => PathTenantResolver::tenantParameterValue($tenant), // path identification
|
||||||
|
'tenant' => $tenant->getTenantKey(), // query string identification
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$newGenerator->defaults($urlGenerator->getDefaultParameters());
|
$newGenerator->defaults($defaultParameters);
|
||||||
|
|
||||||
$newGenerator->setSessionResolver(function () {
|
$newGenerator->setSessionResolver(function () {
|
||||||
return $this->app['session'] ?? null;
|
return $this->app['session'] ?? null;
|
||||||
});
|
|
||||||
|
|
||||||
$newGenerator->setKeyResolver(function () {
|
|
||||||
return $this->app->make('config')->get('app.key');
|
|
||||||
});
|
|
||||||
|
|
||||||
return $newGenerator;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$newGenerator->setKeyResolver(function () {
|
||||||
|
return $this->app->make('config')->get('app.key');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->extend('url', fn () => $newGenerator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,6 @@ trait DealsWithTenantSymlinks
|
||||||
/** Determine if the provided path is an existing symlink. */
|
/** Determine if the provided path is an existing symlink. */
|
||||||
protected function symlinkExists(string $link): bool
|
protected function symlinkExists(string $link): bool
|
||||||
{
|
{
|
||||||
return file_exists($link) && is_link($link);
|
return is_link($link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,5 @@ interface UniqueIdentifierGenerator
|
||||||
/**
|
/**
|
||||||
* Generate a unique identifier for a model.
|
* Generate a unique identifier for a model.
|
||||||
*/
|
*/
|
||||||
public static function generate(Model $model): string;
|
public static function generate(Model $model): string|int;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,11 @@ trait CreatesDatabaseUsers
|
||||||
{
|
{
|
||||||
public function createDatabase(TenantWithDatabase $tenant): bool
|
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||||
{
|
{
|
||||||
parent::createDatabase($tenant);
|
return parent::createDatabase($tenant) && $this->createUser($tenant->database());
|
||||||
|
|
||||||
return $this->createUser($tenant->database());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||||
{
|
{
|
||||||
// Some DB engines require the user to be deleted before the database (e.g. Postgres)
|
return $this->deleteUser($tenant->database()) && parent::deleteDatabase($tenant);
|
||||||
$this->deleteUser($tenant->database());
|
|
||||||
|
|
||||||
return parent::deleteDatabase($tenant);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ use Stancl\Tenancy\Events\PullingPendingTenant;
|
||||||
*/
|
*/
|
||||||
trait HasPending
|
trait HasPending
|
||||||
{
|
{
|
||||||
|
public static string $pendingSinceCast = 'timestamp';
|
||||||
|
|
||||||
/** Boot the trait. */
|
/** Boot the trait. */
|
||||||
public static function bootHasPending(): void
|
public static function bootHasPending(): void
|
||||||
{
|
{
|
||||||
|
|
@ -32,7 +34,7 @@ trait HasPending
|
||||||
/** Initialize the trait. */
|
/** Initialize the trait. */
|
||||||
public function initializeHasPending(): void
|
public function initializeHasPending(): void
|
||||||
{
|
{
|
||||||
$this->casts['pending_since'] = 'timestamp';
|
$this->casts['pending_since'] = static::$pendingSinceCast;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Determine if the model instance is in a pending state. */
|
/** Determine if the model instance is in a pending state. */
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,13 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Database\Concerns;
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
|
||||||
use Stancl\Tenancy\Tenancy;
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
trait InvalidatesResolverCache
|
trait InvalidatesResolverCache
|
||||||
{
|
{
|
||||||
public static function bootInvalidatesResolverCache(): void
|
public static function bootInvalidatesResolverCache(): void
|
||||||
{
|
{
|
||||||
static::saved(function (Tenant&Model $tenant) {
|
static::saved(Tenancy::invalidateResolverCache(...));
|
||||||
Tenancy::invalidateResolverCache($tenant);
|
static::deleting(Tenancy::invalidateResolverCache(...));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ 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\Contracts\CachedTenantResolver;
|
|
||||||
use Stancl\Tenancy\Tenancy;
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -15,13 +14,9 @@ trait InvalidatesTenantsResolverCache
|
||||||
{
|
{
|
||||||
public static function bootInvalidatesTenantsResolverCache(): void
|
public static function bootInvalidatesTenantsResolverCache(): void
|
||||||
{
|
{
|
||||||
static::saved(function (Model $model) {
|
$invalidateCache = static fn (Model $model) => Tenancy::invalidateResolverCache($model->tenant);
|
||||||
foreach (Tenancy::cachedResolvers() as $resolver) {
|
|
||||||
/** @var CachedTenantResolver $resolver */
|
|
||||||
$resolver = app($resolver);
|
|
||||||
|
|
||||||
$resolver->invalidateCache($model->tenant);
|
static::saved($invalidateCache);
|
||||||
}
|
static::deleting($invalidateCache);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ class ImpersonationToken extends Model
|
||||||
{
|
{
|
||||||
use CentralConnection;
|
use CentralConnection;
|
||||||
|
|
||||||
|
/** You can set this property to customize the table name */
|
||||||
|
public static string $tableName = 'tenant_user_impersonation_tokens';
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
@ -33,11 +36,15 @@ class ImpersonationToken extends Model
|
||||||
|
|
||||||
public $incrementing = false;
|
public $incrementing = false;
|
||||||
|
|
||||||
protected $table = 'tenant_user_impersonation_tokens';
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'created_at' => 'datetime',
|
'created_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getTable()
|
||||||
|
{
|
||||||
|
return static::$tableName;
|
||||||
|
}
|
||||||
|
|
||||||
public static function booted(): void
|
public static function booted(): void
|
||||||
{
|
{
|
||||||
static::creating(function ($model) {
|
static::creating(function ($model) {
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ class MicrosoftSQLDatabaseManager extends TenantDatabaseManager
|
||||||
public function createDatabase(TenantWithDatabase $tenant): bool
|
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||||
{
|
{
|
||||||
$database = $tenant->database()->getName();
|
$database = $tenant->database()->getName();
|
||||||
$charset = $this->connection()->getConfig('charset');
|
|
||||||
$collation = $this->connection()->getConfig('collation'); // todo check why these are not used
|
|
||||||
|
|
||||||
return $this->connection()->statement("CREATE DATABASE [{$database}]");
|
return $this->connection()->statement("CREATE DATABASE [{$database}]");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ class PermissionControlledPostgreSQLSchemaManager extends PostgreSQLSchemaManage
|
||||||
// Grant permissions to any existing tables. This is used with RLS
|
// Grant permissions to any existing tables. This is used with RLS
|
||||||
// todo@samuel refactor this along with the todo in TenantDatabaseManager
|
// todo@samuel refactor this along with the todo in TenantDatabaseManager
|
||||||
// and move the grantPermissions() call inside the condition in `ManagesPostgresUsers::createUser()`
|
// and move the grantPermissions() call inside the condition in `ManagesPostgresUsers::createUser()`
|
||||||
|
// but maybe moving it inside $createUser is wrong because some central user may migrate new tables
|
||||||
|
// while the RLS user should STILL get access to those tables
|
||||||
foreach ($tables as $table) {
|
foreach ($tables as $table) {
|
||||||
$tableName = $table->table_name;
|
$tableName = $table->table_name;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,11 @@ 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 // todo we could add a feature to JobPipeline that automatically gets data for the send() from here
|
abstract class TenantEvent
|
||||||
{
|
{
|
||||||
use SerializesModels;
|
use SerializesModels;
|
||||||
|
|
||||||
/** @var Tenant */
|
public function __construct(
|
||||||
public $tenant;
|
public Tenant $tenant,
|
||||||
|
) {}
|
||||||
public function __construct(Tenant $tenant)
|
|
||||||
{
|
|
||||||
$this->tenant = $tenant;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
// todo perhaps create Identification namespace
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Exceptions;
|
namespace Stancl\Tenancy\Exceptions;
|
||||||
|
|
||||||
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Features;
|
namespace Stancl\Tenancy\Features;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Stancl\Tenancy\Contracts\Feature;
|
use Stancl\Tenancy\Contracts\Feature;
|
||||||
|
|
@ -18,8 +19,8 @@ class UserImpersonation implements Feature
|
||||||
|
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
public function bootstrap(Tenancy $tenancy): void
|
||||||
{
|
{
|
||||||
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): ImpersonationToken {
|
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): Model {
|
||||||
return ImpersonationToken::create([
|
return UserImpersonation::modelClass()::create([
|
||||||
Tenancy::tenantKeyColumn() => $tenant->getTenantKey(),
|
Tenancy::tenantKeyColumn() => $tenant->getTenantKey(),
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'redirect_url' => $redirectUrl,
|
'redirect_url' => $redirectUrl,
|
||||||
|
|
@ -30,10 +31,15 @@ class UserImpersonation implements Feature
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Impersonate a user and get an HTTP redirect response. */
|
/** Impersonate a user and get an HTTP redirect response. */
|
||||||
public static function makeResponse(#[\SensitiveParameter] string|ImpersonationToken $token, ?int $ttl = null): RedirectResponse
|
public static function makeResponse(#[\SensitiveParameter] string|Model $token, ?int $ttl = null): RedirectResponse
|
||||||
{
|
{
|
||||||
/** @var ImpersonationToken $token */
|
/**
|
||||||
$token = $token instanceof ImpersonationToken ? $token : ImpersonationToken::findOrFail($token);
|
* The model does NOT have to extend ImpersonationToken, but usually it WILL be a child
|
||||||
|
* of ImpersonationToken and this makes it clear to phpstan that the model has a redirect_url property.
|
||||||
|
*
|
||||||
|
* @var ImpersonationToken $token
|
||||||
|
*/
|
||||||
|
$token = $token instanceof Model ? $token : static::modelClass()::findOrFail($token);
|
||||||
$ttl ??= static::$ttl;
|
$ttl ??= static::$ttl;
|
||||||
|
|
||||||
$tokenExpired = $token->created_at->diffInSeconds(now()) > $ttl;
|
$tokenExpired = $token->created_at->diffInSeconds(now()) > $ttl;
|
||||||
|
|
@ -54,6 +60,12 @@ class UserImpersonation implements Feature
|
||||||
return redirect($token->redirect_url);
|
return redirect($token->redirect_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return class-string<Model> */
|
||||||
|
public static function modelClass(): string
|
||||||
|
{
|
||||||
|
return config('tenancy.models.impersonation_token');
|
||||||
|
}
|
||||||
|
|
||||||
public static function isImpersonating(): bool
|
public static function isImpersonating(): bool
|
||||||
{
|
{
|
||||||
return session()->has('tenancy_impersonating');
|
return session()->has('tenancy_impersonating');
|
||||||
|
|
@ -62,7 +74,7 @@ class UserImpersonation implements Feature
|
||||||
/**
|
/**
|
||||||
* Logout from the current domain and forget impersonation session.
|
* Logout from the current domain and forget impersonation session.
|
||||||
*/
|
*/
|
||||||
public static function leave(): void // todo@name possibly rename
|
public static function stopImpersonating(): void
|
||||||
{
|
{
|
||||||
auth()->logout();
|
auth()->logout();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ use Illuminate\Routing\Events\RouteMatched;
|
||||||
use Stancl\Tenancy\Enums\RouteMode;
|
use Stancl\Tenancy\Enums\RouteMode;
|
||||||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
|
|
||||||
|
// todo@earlyIdReview
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the tenant parameter from the matched route when path identification is used globally.
|
* Remove the tenant parameter from the matched route when path identification is used globally.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class PreventAccessFromUnwantedDomains
|
||||||
return in_array($request->getHost(), config('tenancy.identification.central_domains'), true);
|
return in_array($request->getHost(), config('tenancy.identification.central_domains'), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo@samuel
|
// todo@samuel technically not an identification middleware but probably ok to keep this here
|
||||||
public function requestHasTenant(Request $request): bool
|
public function requestHasTenant(Request $request): bool
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class ScopeSessions
|
||||||
public function handle(Request $request, Closure $next): mixed
|
public function handle(Request $request, Closure $next): mixed
|
||||||
{
|
{
|
||||||
if (! tenancy()->initialized) {
|
if (! tenancy()->initialized) {
|
||||||
|
if (tenancy()->routeIsUniversal(tenancy()->getRoute($request))) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
throw new TenancyNotInitializedException('Tenancy needs to be initialized before the session scoping middleware is executed');
|
throw new TenancyNotInitializedException('Tenancy needs to be initialized before the session scoping middleware is executed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ namespace Stancl\Tenancy\Overrides;
|
||||||
|
|
||||||
use Illuminate\Cache\CacheManager as BaseCacheManager;
|
use Illuminate\Cache\CacheManager as BaseCacheManager;
|
||||||
|
|
||||||
// todo@move move to Cache namespace?
|
|
||||||
|
|
||||||
class CacheManager extends BaseCacheManager
|
class CacheManager extends BaseCacheManager
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,39 +13,85 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
/**
|
/**
|
||||||
* This class is used in place of the default UrlGenerator when UrlGeneratorBootstrapper is enabled.
|
* This class is used in place of the default UrlGenerator when UrlGeneratorBootstrapper is enabled.
|
||||||
*
|
*
|
||||||
* TenancyUrlGenerator does two extra things:
|
* TenancyUrlGenerator does a few extra things:
|
||||||
* 1. Autofill the {tenant} parameter in the tenant context with the current tenant if $passTenantParameterToRoutes is enabled (enabled by default)
|
* - Autofills the tenant parameter in the tenant context with the current tenant.
|
||||||
* 2. Prepend the route name with `tenant.` (or the configured prefix) if $prefixRouteNames is enabled (disabled by default)
|
* This is done either by:
|
||||||
|
* - URL::defaults() -- if UrlGeneratorBootstrapper::$addTenantParameterToDefaults is enabled.
|
||||||
|
* This generally has the best support since tools like e.g. Ziggy read defaults().
|
||||||
|
* - Automatically passing ['tenant' => ...] to each route() call -- if TenancyUrlGenerator::$passTenantParameterToRoutes is enabled
|
||||||
|
* This is a more universal solution since it supports both path identification and query parameter identification.
|
||||||
*
|
*
|
||||||
* Both of these can be skipped by passing the $bypassParameter (`['central' => true]` by default)
|
* - Prepends route names passed to route() and URL::temporarySignedRoute()
|
||||||
|
* with `tenant.` (or the configured prefix) if $prefixRouteNames is enabled.
|
||||||
|
* This is primarily useful when using route cloning with path identification.
|
||||||
|
*
|
||||||
|
* To bypass this behavior on any single route() call, pass the $bypassParameter as true (['central' => true] by default).
|
||||||
*/
|
*/
|
||||||
class TenancyUrlGenerator extends UrlGenerator
|
class TenancyUrlGenerator extends UrlGenerator
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Parameter which bypasses the behavior modification of route() and temporarySignedRoute().
|
* Parameter which works as a flag for bypassing the behavior modification of route() and temporarySignedRoute().
|
||||||
*
|
*
|
||||||
* E.g. route('tenant') => app.test/{tenant}/tenant (or app.test/tenant?tenant=tenantKey if the route doesn't accept the tenant parameter)
|
* For example, in tenant context:
|
||||||
* route('tenant', [$bypassParameter => true]) => app.test/tenant.
|
* Route::get('/', ...)->name('home');
|
||||||
|
* // query string identification
|
||||||
|
* Route::get('/tenant', ...)->middleware(InitializeTenancyByRequestData::class)->name('tenant.home');
|
||||||
|
* - route('home') => app.test/tenant?tenant=tenantKey
|
||||||
|
* - route('home', [$bypassParameter => true]) => app.test/
|
||||||
|
* - route('tenant.home', [$bypassParameter => true]) => app.test/tenant -- no query string added
|
||||||
|
*
|
||||||
|
* Note: UrlGeneratorBootstrapper::$addTenantParameterToDefaults is not affected by this, though
|
||||||
|
* it doesn't matter since it doesn't pass any extra parameters when not needed.
|
||||||
|
*
|
||||||
|
* @see UrlGeneratorBootstrapper
|
||||||
*/
|
*/
|
||||||
public static string $bypassParameter = 'central';
|
public static string $bypassParameter = 'central';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the route names passed to `route()` or `temporarySignedRoute()`
|
* Should route names passed to route() or temporarySignedRoute()
|
||||||
* should get prefixed with the tenant route name prefix.
|
* get prefixed with the tenant route name prefix.
|
||||||
*
|
*
|
||||||
* This is useful when using path identification with packages that generate URLs,
|
* This is useful when using e.g. path identification with third-party packages
|
||||||
* like Jetstream, so that you don't have to manually prefix route names passed to each route() call.
|
* where you don't have control over all route() calls or don't want to change
|
||||||
|
* too many files. Often this will be when using route cloning.
|
||||||
*/
|
*/
|
||||||
public static bool $prefixRouteNames = false;
|
public static bool $prefixRouteNames = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the tenant parameter should get passed
|
* Should the tenant parameter be passed to route() or temporarySignedRoute() calls.
|
||||||
* to the links generated by `route()` or `temporarySignedRoute()` whenever available
|
|
||||||
* (enabled by default – works with both path and query string identification).
|
|
||||||
*
|
*
|
||||||
* With path identification, you can disable this and use URL::defaults() instead (as an alternative solution).
|
* This is useful with path or query parameter identification. The former can be handled
|
||||||
|
* more elegantly using UrlGeneratorBootstrapper::$addTenantParameterToDefaults.
|
||||||
|
*
|
||||||
|
* @see UrlGeneratorBootstrapper
|
||||||
*/
|
*/
|
||||||
public static bool $passTenantParameterToRoutes = true;
|
public static bool $passTenantParameterToRoutes = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route name overrides.
|
||||||
|
*
|
||||||
|
* Note: This behavior can be bypassed using $bypassParameter just like
|
||||||
|
* $prefixRouteNames and $passTenantParameterToRoutes.
|
||||||
|
*
|
||||||
|
* Example from a Jetstream integration:
|
||||||
|
* [
|
||||||
|
* 'profile.show' => 'tenant.profile.show',
|
||||||
|
* 'two-factor.login' => 'tenant.two-factor.login',
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* In the tenant context:
|
||||||
|
* - `route('profile.show')` will return a URL as if you called `route('tenant.profile.show')`.
|
||||||
|
* - `route('profile.show', ['central' => true])` will return a URL as if you called `route('profile.show')`.
|
||||||
|
*/
|
||||||
|
public static array $overrides = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use default parameter names ('tenant' name and tenant key value) instead of the parameter name
|
||||||
|
* and column name configured in the path resolver config.
|
||||||
|
*
|
||||||
|
* You want to enable this when using query string identification while having customized that config.
|
||||||
|
*/
|
||||||
|
public static bool $defaultParameterNames = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the route() method so that the route name gets prefixed
|
* Override the route() method so that the route name gets prefixed
|
||||||
|
|
@ -99,7 +145,7 @@ class TenancyUrlGenerator extends UrlGenerator
|
||||||
protected function prepareRouteInputs(string $name, array $parameters): array
|
protected function prepareRouteInputs(string $name, array $parameters): array
|
||||||
{
|
{
|
||||||
if (! $this->routeBehaviorModificationBypassed($parameters)) {
|
if (! $this->routeBehaviorModificationBypassed($parameters)) {
|
||||||
$name = $this->prefixRouteName($name);
|
$name = $this->routeNameOverride($name) ?? $this->prefixRouteName($name);
|
||||||
$parameters = $this->addTenantParameter($parameters);
|
$parameters = $this->addTenantParameter($parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,10 +170,23 @@ class TenancyUrlGenerator extends UrlGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `tenant()` isn't null, add tenant paramter to the passed parameters.
|
* If `tenant()` isn't null, add the tenant parameter to the passed parameters.
|
||||||
*/
|
*/
|
||||||
protected function addTenantParameter(array $parameters): array
|
protected function addTenantParameter(array $parameters): array
|
||||||
{
|
{
|
||||||
return tenant() && static::$passTenantParameterToRoutes ? array_merge($parameters, [PathTenantResolver::tenantParameterName() => tenant()->getTenantKey()]) : $parameters;
|
if (tenant() && static::$passTenantParameterToRoutes) {
|
||||||
|
if (static::$defaultParameterNames) {
|
||||||
|
return array_merge($parameters, ['tenant' => tenant()->getTenantKey()]);
|
||||||
|
} else {
|
||||||
|
return array_merge($parameters, [PathTenantResolver::tenantParameterName() => PathTenantResolver::tenantParameterValue(tenant())]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return $parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function routeNameOverride(string $name): string|null
|
||||||
|
{
|
||||||
|
return static::$overrides[$name] ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,10 @@ class TableRLSManager implements RLSPolicyManager
|
||||||
$builder = $this->database->getSchemaBuilder();
|
$builder = $this->database->getSchemaBuilder();
|
||||||
|
|
||||||
// We loop through each table in the database
|
// We loop through each table in the database
|
||||||
foreach ($builder->getTableListing() as $table) {
|
foreach ($builder->getTableListing(schema: $this->database->getConfig('search_path')) as $table) {
|
||||||
|
// E.g. "public.table_name" -> "table_name"
|
||||||
|
$table = str($table)->afterLast('.')->toString();
|
||||||
|
|
||||||
// For each table, we get a list of all foreign key columns
|
// For each table, we get a list of all foreign key columns
|
||||||
$foreignKeys = collect($builder->getForeignKeys($table))->map(function ($foreign) use ($table) {
|
$foreignKeys = collect($builder->getForeignKeys($table))->map(function ($foreign) use ($table) {
|
||||||
return $this->formatForeignKey($foreign, $table);
|
return $this->formatForeignKey($foreign, $table);
|
||||||
|
|
@ -105,6 +108,12 @@ class TableRLSManager implements RLSPolicyManager
|
||||||
|
|
||||||
protected function generatePaths(string $table, array $foreign, array &$paths, array $currentPath = []): void
|
protected function generatePaths(string $table, array $foreign, array &$paths, array $currentPath = []): void
|
||||||
{
|
{
|
||||||
|
// If the foreign key has a comment of 'no-rls', we skip it
|
||||||
|
// Also skip the foreign key if implicit scoping is off and the foreign key has no comment
|
||||||
|
if ($foreign['comment'] === 'no-rls' || (! static::$scopeByDefault && $foreign['comment'] === null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (in_array($foreign['foreignTable'], array_column($currentPath, 'foreignTable'))) {
|
if (in_array($foreign['foreignTable'], array_column($currentPath, 'foreignTable'))) {
|
||||||
throw new RecursiveRelationshipException;
|
throw new RecursiveRelationshipException;
|
||||||
}
|
}
|
||||||
|
|
@ -112,15 +121,7 @@ class TableRLSManager implements RLSPolicyManager
|
||||||
$currentPath[] = $foreign;
|
$currentPath[] = $foreign;
|
||||||
|
|
||||||
if ($foreign['foreignTable'] === tenancy()->model()->getTable()) {
|
if ($foreign['foreignTable'] === tenancy()->model()->getTable()) {
|
||||||
$comments = array_column($currentPath, 'comment');
|
$paths[] = $currentPath;
|
||||||
$pathCanUseRls = static::$scopeByDefault ?
|
|
||||||
! in_array('no-rls', $comments) :
|
|
||||||
! in_array('no-rls', $comments) && ! in_array(null, $comments);
|
|
||||||
|
|
||||||
if ($pathCanUseRls) {
|
|
||||||
// If the foreign table is the tenants table, add the current path to $paths
|
|
||||||
$paths[] = $currentPath;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// If not, recursively generate paths for the foreign table
|
// If not, recursively generate paths for the foreign table
|
||||||
foreach ($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable']) as $nextConstraint) {
|
foreach ($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable']) as $nextConstraint) {
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
||||||
|
|
||||||
public static function tenantRouteNamePrefix(): string
|
public static function tenantRouteNamePrefix(): string
|
||||||
{
|
{
|
||||||
return config('tenancy.identification.resolvers.' . static::class . '.tenant_route_name_prefix') ?? static::tenantParameterName() . '.';
|
return config('tenancy.identification.resolvers.' . static::class . '.tenant_route_name_prefix') ?? 'tenant.';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function tenantModelColumn(): string
|
public static function tenantModelColumn(): string
|
||||||
|
|
@ -81,6 +81,11 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
||||||
return config('tenancy.identification.resolvers.' . static::class . '.tenant_model_column') ?? tenancy()->model()->getTenantKeyName();
|
return config('tenancy.identification.resolvers.' . static::class . '.tenant_model_column') ?? tenancy()->model()->getTenantKeyName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function tenantParameterValue(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
return $tenant->getAttribute(static::tenantModelColumn());
|
||||||
|
}
|
||||||
|
|
||||||
/** @return string[] */
|
/** @return string[] */
|
||||||
public static function allowedExtraModelColumns(): array
|
public static function allowedExtraModelColumns(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
|
|
||||||
// todo@move move all resource syncing-related things to a separate namespace?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property-read TenantWithDatabase[]|Collection<int, TenantWithDatabase&Model> $tenants
|
* @property-read TenantWithDatabase[]|Collection<int, TenantWithDatabase&Model> $tenants
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -77,18 +77,21 @@ class Tenancy
|
||||||
public function run(Tenant $tenant, Closure $callback): mixed
|
public function run(Tenant $tenant, Closure $callback): mixed
|
||||||
{
|
{
|
||||||
$originalTenant = $this->tenant;
|
$originalTenant = $this->tenant;
|
||||||
|
$result = null;
|
||||||
|
|
||||||
$this->initialize($tenant);
|
try {
|
||||||
$result = $callback($tenant);
|
$this->initialize($tenant);
|
||||||
|
$result = $callback($tenant);
|
||||||
|
} finally {
|
||||||
|
if ($result instanceof PendingDispatch) { // #1277
|
||||||
|
$result = null;
|
||||||
|
}
|
||||||
|
|
||||||
if ($result instanceof PendingDispatch) { // #1277
|
if ($originalTenant) {
|
||||||
$result = null;
|
$this->initialize($originalTenant);
|
||||||
}
|
} else {
|
||||||
|
$this->end();
|
||||||
if ($originalTenant) {
|
}
|
||||||
$this->initialize($originalTenant);
|
|
||||||
} else {
|
|
||||||
$this->end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
@ -204,8 +207,10 @@ class Tenancy
|
||||||
// Wrap string in array
|
// Wrap string in array
|
||||||
$tenants = is_string($tenants) ? [$tenants] : $tenants;
|
$tenants = is_string($tenants) ? [$tenants] : $tenants;
|
||||||
|
|
||||||
// Use all tenants if $tenants is falsy
|
// If $tenants is falsy by this point (e.g. an empty array) there's no work to be done
|
||||||
$tenants = $tenants ?: $this->model()->cursor(); // todo@phpstan phpstan thinks this isn't needed, but tests fail without it
|
if (! $tenants) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$originalTenant = $this->tenant;
|
$originalTenant = $this->tenant;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy;
|
namespace Stancl\Tenancy;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
use Illuminate\Cache\CacheManager;
|
use Illuminate\Cache\CacheManager;
|
||||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||||
use Illuminate\Routing\Events\RouteMatched;
|
use Illuminate\Routing\Events\RouteMatched;
|
||||||
|
|
@ -18,9 +19,15 @@ use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
|
||||||
class TenancyServiceProvider extends ServiceProvider
|
class TenancyServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
public static Closure|null $configure = null;
|
||||||
|
|
||||||
/* Register services. */
|
/* Register services. */
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
|
if (static::$configure) {
|
||||||
|
(static::$configure)();
|
||||||
|
}
|
||||||
|
|
||||||
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy');
|
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy');
|
||||||
|
|
||||||
$this->app->singleton(Database\DatabaseManager::class);
|
$this->app->singleton(Database\DatabaseManager::class);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class RandomHexGenerator implements UniqueIdentifierGenerator
|
||||||
{
|
{
|
||||||
public static int $bytes = 6;
|
public static int $bytes = 6;
|
||||||
|
|
||||||
public static function generate(Model $model): string
|
public static function generate(Model $model): string|int
|
||||||
{
|
{
|
||||||
return bin2hex(random_bytes(static::$bytes));
|
return bin2hex(random_bytes(static::$bytes));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
src/UniqueIdentifierGenerators/RandomIntGenerator.php
Normal file
22
src/UniqueIdentifierGenerators/RandomIntGenerator.php
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\UniqueIdentifierGenerators;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a cryptographically secure random integer for the tenant key.
|
||||||
|
*/
|
||||||
|
class RandomIntGenerator implements UniqueIdentifierGenerator
|
||||||
|
{
|
||||||
|
public static int $min = 0;
|
||||||
|
public static int $max = PHP_INT_MAX;
|
||||||
|
|
||||||
|
public static function generate(Model $model): string|int
|
||||||
|
{
|
||||||
|
return random_int(static::$min, static::$max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ class RandomStringGenerator implements UniqueIdentifierGenerator
|
||||||
{
|
{
|
||||||
public static int $length = 8;
|
public static int $length = 8;
|
||||||
|
|
||||||
public static function generate(Model $model): string
|
public static function generate(Model $model): string|int
|
||||||
{
|
{
|
||||||
return Str::random(static::$length);
|
return Str::random(static::$length);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
src/UniqueIdentifierGenerators/ULIDGenerator.php
Normal file
20
src/UniqueIdentifierGenerators/ULIDGenerator.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\UniqueIdentifierGenerators;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a UUID for the tenant key.
|
||||||
|
*/
|
||||||
|
class ULIDGenerator implements UniqueIdentifierGenerator
|
||||||
|
{
|
||||||
|
public static function generate(Model $model): string|int
|
||||||
|
{
|
||||||
|
return Str::ulid()->toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||||
*/
|
*/
|
||||||
class UUIDGenerator implements UniqueIdentifierGenerator
|
class UUIDGenerator implements UniqueIdentifierGenerator
|
||||||
{
|
{
|
||||||
public static function generate(Model $model): string
|
public static function generate(Model $model): string|int
|
||||||
{
|
{
|
||||||
return Uuid::uuid4()->toString();
|
return Uuid::uuid4()->toString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
|
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
|
||||||
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
|
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
|
@ -35,11 +36,15 @@ test('create storage symlinks action works', function() {
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
$this->assertDirectoryDoesNotExist($publicPath = public_path("public-$tenantKey"));
|
// The symlink doesn't exist
|
||||||
|
expect(is_link($publicPath = public_path("public-$tenantKey")))->toBeFalse();
|
||||||
|
expect(file_exists($publicPath))->toBeFalse();
|
||||||
|
|
||||||
(new CreateStorageSymlinksAction)($tenant);
|
(new CreateStorageSymlinksAction)($tenant);
|
||||||
|
|
||||||
$this->assertDirectoryExists($publicPath);
|
// The symlink exists and is valid
|
||||||
|
expect(is_link($publicPath = public_path("public-$tenantKey")))->toBeTrue();
|
||||||
|
expect(file_exists($publicPath))->toBeTrue();
|
||||||
$this->assertEquals(storage_path("app/public/"), readlink($publicPath));
|
$this->assertEquals(storage_path("app/public/"), readlink($publicPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -61,9 +66,48 @@ test('remove storage symlinks action works', function() {
|
||||||
|
|
||||||
(new CreateStorageSymlinksAction)($tenant);
|
(new CreateStorageSymlinksAction)($tenant);
|
||||||
|
|
||||||
$this->assertDirectoryExists($publicPath = public_path("public-$tenantKey"));
|
// The symlink exists and is valid
|
||||||
|
expect(is_link($publicPath = public_path("public-$tenantKey")))->toBeTrue();
|
||||||
|
expect(file_exists($publicPath))->toBeTrue();
|
||||||
|
|
||||||
(new RemoveStorageSymlinksAction)($tenant);
|
(new RemoveStorageSymlinksAction)($tenant);
|
||||||
|
|
||||||
$this->assertDirectoryDoesNotExist($publicPath);
|
// The symlink doesn't exist
|
||||||
|
expect(is_link($publicPath))->toBeFalse();
|
||||||
|
expect(file_exists($publicPath))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removing tenant symlinks works even if the symlinks are invalid', function() {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [
|
||||||
|
FilesystemTenancyBootstrapper::class,
|
||||||
|
],
|
||||||
|
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||||
|
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||||
|
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var Tenant $tenant */
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$tenantKey = $tenant->getTenantKey();
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
(new CreateStorageSymlinksAction)($tenant);
|
||||||
|
|
||||||
|
// The symlink exists and is valid
|
||||||
|
expect(is_link($publicPath = public_path("public-$tenantKey")))->toBeTrue();
|
||||||
|
expect(file_exists($publicPath))->toBeTrue();
|
||||||
|
|
||||||
|
// Make the symlink invalid by deleting the tenant storage directory
|
||||||
|
$storagePath = storage_path();
|
||||||
|
File::deleteDirectory($storagePath);
|
||||||
|
|
||||||
|
// The symlink still exists, but isn't valid
|
||||||
|
expect(is_link($publicPath))->toBeTrue();
|
||||||
|
expect(file_exists($publicPath))->toBeFalse();
|
||||||
|
|
||||||
|
(new RemoveStorageSymlinksAction)($tenant);
|
||||||
|
|
||||||
|
expect(is_link($publicPath))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$this->mockConsoleOutput = false;
|
$this->mockConsoleOutput = false;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
||||||
use Stancl\Tenancy\Events\TenancyEnded;
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config(['tenancy.bootstrappers' => [CacheTagsBootstrapper::class]]);
|
config(['tenancy.bootstrappers' => [CacheTagsBootstrapper::class]]);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
use Stancl\Tenancy\Listeners;
|
use Stancl\Tenancy\Listeners;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This collection of regression tests verifies that SessionTenancyBootstrapper
|
* This collection of regression tests verifies that SessionTenancyBootstrapper
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
|
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,14 @@ use Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteBootstrapper;
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
FortifyRouteBootstrapper::$passTenantParameter = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
FortifyRouteBootstrapper::$passTenantParameter = true;
|
||||||
|
FortifyRouteBootstrapper::$fortifyRedirectMap = [];
|
||||||
|
FortifyRouteBootstrapper::$fortifyHome = 'tenant.dashboard';
|
||||||
|
FortifyRouteBootstrapper::$defaultParameterNames = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fortify route tenancy bootstrapper updates fortify config correctly', function() {
|
test('fortify route tenancy bootstrapper updates fortify config correctly', function() {
|
||||||
|
|
@ -25,53 +33,31 @@ test('fortify route tenancy bootstrapper updates fortify config correctly', func
|
||||||
return true;
|
return true;
|
||||||
})->name($homeRouteName = 'home');
|
})->name($homeRouteName = 'home');
|
||||||
|
|
||||||
Route::get('/{tenant}/home', function () {
|
|
||||||
return true;
|
|
||||||
})->name($pathIdHomeRouteName = 'tenant.home');
|
|
||||||
|
|
||||||
Route::get('/welcome', function () {
|
Route::get('/welcome', function () {
|
||||||
return true;
|
return true;
|
||||||
})->name($welcomeRouteName = 'welcome');
|
})->name($welcomeRouteName = 'welcome');
|
||||||
|
|
||||||
Route::get('/{tenant}/welcome', function () {
|
|
||||||
return true;
|
|
||||||
})->name($pathIdWelcomeRouteName = 'path.welcome');
|
|
||||||
|
|
||||||
FortifyRouteBootstrapper::$fortifyHome = $homeRouteName;
|
FortifyRouteBootstrapper::$fortifyHome = $homeRouteName;
|
||||||
|
FortifyRouteBootstrapper::$fortifyRedirectMap['login'] = $welcomeRouteName;
|
||||||
|
|
||||||
// Make login redirect to the central welcome route
|
expect(config('fortify.home'))->toBe($originalFortifyHome);
|
||||||
FortifyRouteBootstrapper::$fortifyRedirectMap['login'] = [
|
expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
|
||||||
'route_name' => $welcomeRouteName,
|
|
||||||
'context' => Context::CENTRAL,
|
|
||||||
];
|
|
||||||
|
|
||||||
|
FortifyRouteBootstrapper::$passTenantParameter = true;
|
||||||
tenancy()->initialize($tenant = Tenant::create());
|
tenancy()->initialize($tenant = Tenant::create());
|
||||||
// The bootstraper makes fortify.home always receive the tenant parameter
|
|
||||||
expect(config('fortify.home'))->toBe('http://localhost/home?tenant=' . $tenant->getTenantKey());
|
expect(config('fortify.home'))->toBe('http://localhost/home?tenant=' . $tenant->getTenantKey());
|
||||||
|
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome?tenant=' . $tenant->getTenantKey()]);
|
||||||
// The login redirect route has the central context specified, so it doesn't receive the tenant parameter
|
|
||||||
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome']);
|
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
expect(config('fortify.home'))->toBe($originalFortifyHome);
|
expect(config('fortify.home'))->toBe($originalFortifyHome);
|
||||||
expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
|
expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
|
||||||
|
|
||||||
// Making a route's context will pass the tenant parameter to the route
|
FortifyRouteBootstrapper::$passTenantParameter = false;
|
||||||
FortifyRouteBootstrapper::$fortifyRedirectMap['login']['context'] = Context::TENANT;
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
expect(config('fortify.home'))->toBe('http://localhost/home');
|
||||||
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome?tenant=' . $tenant->getTenantKey()]);
|
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome']);
|
||||||
|
|
||||||
// Make the home and login route accept the tenant as a route parameter
|
|
||||||
// To confirm that tenant route parameter gets filled automatically too (path identification works as well as query string)
|
|
||||||
FortifyRouteBootstrapper::$fortifyHome = $pathIdHomeRouteName;
|
|
||||||
FortifyRouteBootstrapper::$fortifyRedirectMap['login']['route_name'] = $pathIdWelcomeRouteName;
|
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
|
expect(config('fortify.home'))->toBe($originalFortifyHome);
|
||||||
tenancy()->initialize($tenant);
|
expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
|
||||||
|
|
||||||
expect(config('fortify.home'))->toBe("http://localhost/{$tenant->getTenantKey()}/home");
|
|
||||||
expect(config('fortify.redirects'))->toEqual(['login' => "http://localhost/{$tenant->getTenantKey()}/welcome"]);
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,23 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
|
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
RootUrlBootstrapper::$rootUrlOverride = null;
|
RootUrlBootstrapper::$rootUrlOverride = null;
|
||||||
|
RootUrlBootstrapper::$rootUrlOverrideInTests = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
RootUrlBootstrapper::$rootUrlOverride = null;
|
RootUrlBootstrapper::$rootUrlOverride = null;
|
||||||
|
RootUrlBootstrapper::$rootUrlOverrideInTests = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('root url bootstrapper overrides the root url when tenancy gets initialized and reverts the url to the central one after tenancy ends', function() {
|
test('root url bootstrapper overrides the root url when tenancy gets initialized and reverts the url to the central one when ending tenancy', function() {
|
||||||
config(['tenancy.bootstrappers' => [RootUrlBootstrapper::class]]);
|
config(['tenancy.bootstrappers' => [RootUrlBootstrapper::class]]);
|
||||||
|
|
||||||
Route::group([
|
Route::group([
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,14 @@ beforeEach(function () {
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
TenancyUrlGenerator::$prefixRouteNames = false;
|
TenancyUrlGenerator::$prefixRouteNames = false;
|
||||||
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
TenancyUrlGenerator::$passTenantParameterToRoutes = false;
|
||||||
|
UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
TenancyUrlGenerator::$prefixRouteNames = false;
|
TenancyUrlGenerator::$prefixRouteNames = false;
|
||||||
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
TenancyUrlGenerator::$passTenantParameterToRoutes = false;
|
||||||
|
UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('url generator bootstrapper swaps the url generator instance correctly', function() {
|
test('url generator bootstrapper swaps the url generator instance correctly', function() {
|
||||||
|
|
@ -41,36 +43,28 @@ test('url generator bootstrapper swaps the url generator instance correctly', fu
|
||||||
->not()->toBeInstanceOf(TenancyUrlGenerator::class);
|
->not()->toBeInstanceOf(TenancyUrlGenerator::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('url generator bootstrapper can prefix route names passed to the route helper', function() {
|
test('tenancy url generator can prefix route names passed to the route helper', function() {
|
||||||
Route::get('/central/home', fn () => route('home'))->name('home');
|
Route::get('/central/home', fn () => route('home'))->name('home');
|
||||||
// Tenant route name prefix is 'tenant.' by default
|
// Tenant route name prefix is 'tenant.' by default
|
||||||
Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]);
|
Route::get('/tenant/home', fn () => route('tenant.home'))->name('tenant.home');
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
$tenantKey = $tenant->getTenantKey();
|
|
||||||
$centralRouteUrl = route('home');
|
$centralRouteUrl = route('home');
|
||||||
$tenantRouteUrl = route('tenant.home', ['tenant' => $tenantKey]);
|
$tenantRouteUrl = route('tenant.home');
|
||||||
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
|
|
||||||
|
|
||||||
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
// Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false
|
// Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false (default)
|
||||||
expect(route('home'))->not()->toBe($centralRouteUrl);
|
expect(route('home'))->toBe($centralRouteUrl);
|
||||||
// When TenancyUrlGenerator::$passTenantParameterToRoutes is true (default)
|
|
||||||
// The route helper receives the tenant parameter
|
|
||||||
// So in order to generate central URL, we have to pass the bypass parameter
|
|
||||||
expect(route('home', ['bypassParameter' => true]))->toBe($centralRouteUrl);
|
|
||||||
|
|
||||||
|
|
||||||
|
// When $prefixRouteNames is true, the route name passed to the route() helper ('home') gets prefixed with 'tenant.' automatically.
|
||||||
TenancyUrlGenerator::$prefixRouteNames = true;
|
TenancyUrlGenerator::$prefixRouteNames = true;
|
||||||
// The $prefixRouteNames property is true
|
|
||||||
// The route name passed to the route() helper ('home') gets prefixed prefixed with 'tenant.' automatically
|
|
||||||
expect(route('home'))->toBe($tenantRouteUrl);
|
expect(route('home'))->toBe($tenantRouteUrl);
|
||||||
|
|
||||||
// The 'tenant.home' route name doesn't get prefixed because it is already prefixed with 'tenant.'
|
// The 'tenant.home' route name doesn't get prefixed -- it is already prefixed with 'tenant.'
|
||||||
// Also, the route receives the tenant parameter automatically
|
|
||||||
expect(route('tenant.home'))->toBe($tenantRouteUrl);
|
expect(route('tenant.home'))->toBe($tenantRouteUrl);
|
||||||
|
|
||||||
// Ending tenancy reverts route() behavior changes
|
// Ending tenancy reverts route() behavior changes
|
||||||
|
|
@ -79,6 +73,76 @@ test('url generator bootstrapper can prefix route names passed to the route help
|
||||||
expect(route('home'))->toBe($centralRouteUrl);
|
expect(route('home'))->toBe($centralRouteUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the route helper can receive the tenant parameter automatically', function (
|
||||||
|
string $identification,
|
||||||
|
bool $addTenantParameterToDefaults,
|
||||||
|
bool $passTenantParameterToRoutes,
|
||||||
|
) {
|
||||||
|
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
||||||
|
|
||||||
|
$appUrl = config('app.url');
|
||||||
|
|
||||||
|
UrlGeneratorBootstrapper::$addTenantParameterToDefaults = $addTenantParameterToDefaults;
|
||||||
|
|
||||||
|
// When the tenant parameter isn't added to defaults, the tenant parameter has to be passed "manually"
|
||||||
|
// by setting $passTenantParameterToRoutes to true. This is only preferable with query string identification.
|
||||||
|
// With path identification, this ultimately doesn't have any effect
|
||||||
|
// if UrlGeneratorBootstrapper::$addTenantParameterToDefaults is true,
|
||||||
|
// but TenancyUrlGenerator::$passTenantParameterToRoutes can still be used instead.
|
||||||
|
TenancyUrlGenerator::$passTenantParameterToRoutes = $passTenantParameterToRoutes;
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$tenantKey = $tenant->getTenantKey();
|
||||||
|
|
||||||
|
Route::get('/central/home', fn () => route('home'))->name('home');
|
||||||
|
|
||||||
|
$tenantRoute = $identification === InitializeTenancyByPath::class ? "/{tenant}/home" : "/tenant/home";
|
||||||
|
|
||||||
|
Route::get($tenantRoute, fn () => route('tenant.home'))
|
||||||
|
->name('tenant.home')
|
||||||
|
->middleware(['tenant', $identification]);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
$expectedUrl = match (true) {
|
||||||
|
$identification === InitializeTenancyByRequestData::class && $passTenantParameterToRoutes => "{$appUrl}/tenant/home?tenant={$tenantKey}",
|
||||||
|
$identification === InitializeTenancyByRequestData::class => "{$appUrl}/tenant/home", // $passTenantParameterToRoutes is false
|
||||||
|
$identification === InitializeTenancyByPath::class && ($addTenantParameterToDefaults || $passTenantParameterToRoutes) => "{$appUrl}/{$tenantKey}/home",
|
||||||
|
$identification === InitializeTenancyByPath::class => null, // Should throw an exception -- route() doesn't receive the tenant parameter in this case
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($expectedUrl === null) {
|
||||||
|
expect(fn () => route('tenant.home'))->toThrow(UrlGenerationException::class, 'Missing parameter: tenant');
|
||||||
|
} else {
|
||||||
|
expect(route('tenant.home'))->toBe($expectedUrl);
|
||||||
|
}
|
||||||
|
})->with([InitializeTenancyByPath::class, InitializeTenancyByRequestData::class])
|
||||||
|
->with([true, false]) // UrlGeneratorBootstrapper::$addTenantParameterToDefaults
|
||||||
|
->with([true, false]); // TenancyUrlGenerator::$passTenantParameterToRoutes
|
||||||
|
|
||||||
|
test('url generator can override specific route names', function() {
|
||||||
|
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
||||||
|
|
||||||
|
Route::get('/foo', fn () => 'foo')->name('foo');
|
||||||
|
Route::get('/bar', fn () => 'bar')->name('bar');
|
||||||
|
Route::get('/baz', fn () => 'baz')->name('baz'); // Not overridden
|
||||||
|
|
||||||
|
TenancyUrlGenerator::$overrides = ['foo' => 'bar'];
|
||||||
|
|
||||||
|
expect(route('foo'))->toBe(url('/foo'));
|
||||||
|
expect(route('bar'))->toBe(url('/bar'));
|
||||||
|
expect(route('baz'))->toBe(url('/baz'));
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create());
|
||||||
|
|
||||||
|
expect(route('foo'))->toBe(url('/bar'));
|
||||||
|
expect(route('bar'))->toBe(url('/bar')); // not overridden
|
||||||
|
expect(route('baz'))->toBe(url('/baz')); // not overridden
|
||||||
|
|
||||||
|
// Bypass the override
|
||||||
|
expect(route('foo', ['central' => true]))->toBe(url('/foo'));
|
||||||
|
});
|
||||||
|
|
||||||
test('both the name prefixing and the tenant parameter logic gets skipped when bypass parameter is used', function () {
|
test('both the name prefixing and the tenant parameter logic gets skipped when bypass parameter is used', function () {
|
||||||
$tenantParameterName = PathTenantResolver::tenantParameterName();
|
$tenantParameterName = PathTenantResolver::tenantParameterName();
|
||||||
|
|
||||||
|
|
@ -105,54 +169,8 @@ test('both the name prefixing and the tenant parameter logic gets skipped when b
|
||||||
->not()->toContain('bypassParameter');
|
->not()->toContain('bypassParameter');
|
||||||
|
|
||||||
// When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home')
|
// When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home')
|
||||||
expect(route('home', ['bypassParameter' => false]))->toBe($tenantRouteUrl)
|
// The tenant parameter is not passed automatically since both
|
||||||
|
// UrlGeneratorBootstrapper::$addTenantParameterToDefaults and TenancyUrlGenerator::$passTenantParameterToRoutes are false by default
|
||||||
|
expect(route('home', ['bypassParameter' => false, 'tenant' => $tenant->getTenantKey()]))->toBe($tenantRouteUrl)
|
||||||
->not()->toContain('bypassParameter');
|
->not()->toContain('bypassParameter');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('url generator bootstrapper can make route helper generate links with the tenant parameter', function() {
|
|
||||||
Route::get('/query_string', fn () => route('query_string'))->name('query_string')->middleware(['universal', InitializeTenancyByRequestData::class]);
|
|
||||||
Route::get('/path', fn () => route('path'))->name('path');
|
|
||||||
Route::get('/{tenant}/path', fn () => route('tenant.path'))->name('tenant.path')->middleware([InitializeTenancyByPath::class]);
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$tenantKey = $tenant->getTenantKey();
|
|
||||||
$queryStringCentralUrl = route('query_string');
|
|
||||||
$queryStringTenantUrl = route('query_string', ['tenant' => $tenantKey]);
|
|
||||||
$pathCentralUrl = route('path');
|
|
||||||
$pathTenantUrl = route('tenant.path', ['tenant' => $tenantKey]);
|
|
||||||
|
|
||||||
// Makes the route helper receive the tenant parameter whenever available
|
|
||||||
// Unless the bypass parameter is true
|
|
||||||
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
|
||||||
|
|
||||||
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
|
|
||||||
|
|
||||||
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
|
||||||
|
|
||||||
expect(route('path'))->toBe($pathCentralUrl);
|
|
||||||
// Tenant parameter required, but not passed since tenancy wasn't initialized
|
|
||||||
expect(fn () => route('tenant.path'))->toThrow(UrlGenerationException::class);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
// Tenant parameter is passed automatically
|
|
||||||
expect(route('path'))->not()->toBe($pathCentralUrl); // Parameter added as query string – bypassParameter needed
|
|
||||||
expect(route('path', ['bypassParameter' => true]))->toBe($pathCentralUrl);
|
|
||||||
expect(route('tenant.path'))->toBe($pathTenantUrl);
|
|
||||||
|
|
||||||
expect(route('query_string'))->toBe($queryStringTenantUrl)->toContain('tenant=');
|
|
||||||
expect(route('query_string', ['bypassParameter' => 'true']))->toBe($queryStringCentralUrl)->not()->toContain('tenant=');
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
expect(route('query_string'))->toBe($queryStringCentralUrl);
|
|
||||||
|
|
||||||
// Tenant parameter required, but shouldn't be passed since tenancy isn't initialized
|
|
||||||
expect(fn () => route('tenant.path'))->toThrow(UrlGenerationException::class);
|
|
||||||
|
|
||||||
// Route-level identification
|
|
||||||
pest()->get("http://localhost/query_string")->assertSee($queryStringCentralUrl);
|
|
||||||
pest()->get("http://localhost/query_string?tenant=$tenantKey")->assertSee($queryStringTenantUrl);
|
|
||||||
pest()->get("http://localhost/path")->assertSee($pathCentralUrl);
|
|
||||||
pest()->get("http://localhost/$tenantKey/path")->assertSee($pathTenantUrl);
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use Illuminate\Broadcasting\Broadcasters\NullBroadcaster;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
||||||
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
|
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
|
||||||
|
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
withTenantDatabases();
|
withTenantDatabases();
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||||
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
use Stancl\Tenancy\PathIdentificationManager;
|
|
||||||
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('tenants can be resolved using cached resolvers', function (string $resolver) {
|
test('tenants can be resolved using cached resolvers', function (string $resolver) {
|
||||||
$tenant = Tenant::create(['id' => $tenantKey = 'acme']);
|
$tenant = Tenant::create(['id' => $tenantKey = 'acme']);
|
||||||
|
|
@ -84,6 +86,34 @@ test('cache is invalidated when the tenant is updated', function (string $resolv
|
||||||
RequestDataTenantResolver::class,
|
RequestDataTenantResolver::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
test('cache is invalidated when the tenant is deleted', function (string $resolver) {
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=0;'); // allow deleting the tenant
|
||||||
|
$tenant = Tenant::create(['id' => $tenantKey = 'acme']);
|
||||||
|
$tenant->createDomain($tenantKey);
|
||||||
|
|
||||||
|
DB::enableQueryLog();
|
||||||
|
|
||||||
|
config(['tenancy.identification.resolvers.' . $resolver . '.cache' => true]);
|
||||||
|
|
||||||
|
expect($tenant->is(app($resolver)->resolve(getResolverArgument($resolver, $tenantKey))))->toBeTrue();
|
||||||
|
expect(DB::getQueryLog())->not()->toBeEmpty();
|
||||||
|
|
||||||
|
DB::flushQueryLog();
|
||||||
|
|
||||||
|
expect($tenant->is(app($resolver)->resolve(getResolverArgument($resolver, $tenantKey))))->toBeTrue();
|
||||||
|
expect(DB::getQueryLog())->toBeEmpty();
|
||||||
|
|
||||||
|
$tenant->delete();
|
||||||
|
DB::flushQueryLog();
|
||||||
|
|
||||||
|
expect(fn () => app($resolver)->resolve(getResolverArgument($resolver, $tenantKey)))->toThrow(TenantCouldNotBeIdentifiedException::class);
|
||||||
|
expect(DB::getQueryLog())->not()->toBeEmpty(); // Cache was invalidated, so the DB was queried
|
||||||
|
})->with([
|
||||||
|
DomainTenantResolver::class,
|
||||||
|
PathTenantResolver::class,
|
||||||
|
RequestDataTenantResolver::class,
|
||||||
|
]);
|
||||||
|
|
||||||
test('cache is invalidated when a tenants domain is changed', function () {
|
test('cache is invalidated when a tenants domain is changed', function () {
|
||||||
$tenant = Tenant::create(['id' => $tenantKey = 'acme']);
|
$tenant = Tenant::create(['id' => $tenantKey = 'acme']);
|
||||||
$tenant->createDomain($tenantKey);
|
$tenant->createDomain($tenantKey);
|
||||||
|
|
@ -110,6 +140,26 @@ test('cache is invalidated when a tenants domain is changed', function () {
|
||||||
pest()->assertNotEmpty(DB::getQueryLog()); // not empty
|
pest()->assertNotEmpty(DB::getQueryLog()); // not empty
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cache is invalidated when a tenants domain is deleted', function () {
|
||||||
|
$tenant = Tenant::create(['id' => $tenantKey = 'acme']);
|
||||||
|
$tenant->createDomain($tenantKey);
|
||||||
|
|
||||||
|
DB::enableQueryLog();
|
||||||
|
|
||||||
|
config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
|
||||||
|
|
||||||
|
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
||||||
|
DB::flushQueryLog();
|
||||||
|
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
||||||
|
expect(DB::getQueryLog())->toBeEmpty(); // empty
|
||||||
|
|
||||||
|
$tenant->domains->first()->delete();
|
||||||
|
DB::flushQueryLog();
|
||||||
|
|
||||||
|
expect(fn () => $tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toThrow(TenantCouldNotBeIdentifiedOnDomainException::class);
|
||||||
|
expect(DB::getQueryLog())->not()->toBeEmpty(); // Cache was invalidated, so the DB was queried
|
||||||
|
});
|
||||||
|
|
||||||
test('PathTenantResolver forgets the tenant route parameter when the tenant is resolved from cache', function() {
|
test('PathTenantResolver forgets the tenant route parameter when the tenant is resolved from cache', function() {
|
||||||
config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.cache' => true]);
|
config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.cache' => true]);
|
||||||
DB::enableQueryLog();
|
DB::enableQueryLog();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Route as RouteFacade;
|
||||||
use Stancl\Tenancy\Tests\Etc\HasMiddlewareController;
|
use Stancl\Tenancy\Tests\Etc\HasMiddlewareController;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('a route can be universal using path identification', function (array $routeMiddleware, array $globalMiddleware) {
|
test('a route can be universal using path identification', function (array $routeMiddleware, array $globalMiddleware) {
|
||||||
foreach ($globalMiddleware as $middleware) {
|
foreach ($globalMiddleware as $middleware) {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,9 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||||||
use Stancl\Tenancy\Database\Models;
|
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Route::group([
|
Route::group([
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
|
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMySQLData
|
||||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager;
|
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager;
|
||||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
|
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
|
||||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
|
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config([
|
config([
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
InitializeTenancyByDomain::$onFail = null;
|
InitializeTenancyByDomain::$onFail = null;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByOriginHeader;
|
||||||
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithMiddleware;
|
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithMiddleware;
|
||||||
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithRouteMiddleware;
|
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithRouteMiddleware;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config()->set([
|
config()->set([
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use Stancl\Tenancy\Events\BootstrappingTenancy;
|
||||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
FooListener::$shouldQueue = false;
|
FooListener::$shouldQueue = false;
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,9 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('sqlite ATTACH statements can be blocked', function (bool $disallow) {
|
test('sqlite ATTACH statements can be blocked', function (bool $disallow) {
|
||||||
try {
|
|
||||||
readlink(base_path('vendor'));
|
|
||||||
} catch (\Throwable) {
|
|
||||||
symlink(base_path('vendor'), '/var/www/html/vendor');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (php_uname('m') == 'aarch64') {
|
if (php_uname('m') == 'aarch64') {
|
||||||
// Escape testbench prison. Can't hardcode /var/www/html/extensions/... here
|
// Escape testbench prison. Can't hardcode /var/www/html/extensions/... here
|
||||||
// since GHA doesn't mount the filesystem on the container's workdir
|
// since GHA doesn't mount the filesystem on the container's workdir
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Features\CrossDomainRedirect;
|
use Stancl\Tenancy\Features\CrossDomainRedirect;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('tenant redirect macro replaces only the hostname', function () {
|
test('tenant redirect macro replaces only the hostname', function () {
|
||||||
config([
|
config([
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use Stancl\Tenancy\Features\TenantConfig;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
TenantConfig::$storageToConfigMap = [];
|
TenantConfig::$storageToConfigMap = [];
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\MailConfigBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\MailConfigBootstrapper;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
config(['mail.default' => 'smtp']);
|
config(['mail.default' => 'smtp']);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
|
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
use Stancl\Tenancy\Listeners\CreateTenantConnection;
|
use Stancl\Tenancy\Listeners\CreateTenantConnection;
|
||||||
use Stancl\Tenancy\Listeners\UseCentralConnection;
|
use Stancl\Tenancy\Listeners\UseCentralConnection;
|
||||||
use Stancl\Tenancy\Listeners\UseTenantConnection;
|
use Stancl\Tenancy\Listeners\UseTenantConnection;
|
||||||
use \Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('manual tenancy initialization works', function () {
|
test('manual tenancy initialization works', function () {
|
||||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByOriginHeader;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByOriginHeader;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
InitializeTenancyByOriginHeader::$onFail = null;
|
InitializeTenancyByOriginHeader::$onFail = null;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
// Make sure the tenant parameter is set to 'tenant'
|
// Make sure the tenant parameter is set to 'tenant'
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use Stancl\Tenancy\Events\PendingTenantCreated;
|
||||||
use Stancl\Tenancy\Events\PendingTenantPulled;
|
use Stancl\Tenancy\Events\PendingTenantPulled;
|
||||||
use Stancl\Tenancy\Events\PullingPendingTenant;
|
use Stancl\Tenancy\Events\PullingPendingTenant;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('tenants are correctly identified as pending', function (){
|
test('tenants are correctly identified as pending', function (){
|
||||||
Tenant::createPending();
|
Tenant::createPending();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Tests;
|
||||||
|
|
||||||
use Stancl\Tenancy\Tests\TestCase;
|
use Stancl\Tenancy\Tests\TestCase;
|
||||||
use Stancl\JobPipeline\JobPipeline;
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
@ -8,14 +10,14 @@ use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
|
||||||
uses(TestCase::class)->in(__DIR__);
|
uses(TestCase::class)->in(__DIR__);
|
||||||
|
|
||||||
function pest(): TestCase
|
|
||||||
{
|
|
||||||
return Pest\TestSuite::getInstance()->test;
|
|
||||||
}
|
|
||||||
|
|
||||||
function withTenantDatabases()
|
function withTenantDatabases()
|
||||||
{
|
{
|
||||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->toListener());
|
})->toListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pest(): TestCase
|
||||||
|
{
|
||||||
|
return \Pest\TestSuite::getInstance()->test;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||||
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||||||
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithMiddleware;
|
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithMiddleware;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('correct routes are accessible in route-level identification', function (RouteMode $defaultRouteMode) {
|
test('correct routes are accessible in route-level identification', function (RouteMode $defaultRouteMode) {
|
||||||
config()->set([
|
config()->set([
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,14 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\PersistentQueueTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$this->mockConsoleOutput = false;
|
|
||||||
|
|
||||||
config([
|
config([
|
||||||
'tenancy.bootstrappers' => [
|
'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class],
|
||||||
QueueTenancyBootstrapper::class,
|
|
||||||
DatabaseTenancyBootstrapper::class,
|
|
||||||
],
|
|
||||||
'queue.default' => 'redis',
|
'queue.default' => 'redis',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -45,7 +43,22 @@ afterEach(function () {
|
||||||
pest()->valuestore->flush();
|
pest()->valuestore->flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant id is passed to tenant queues', function () {
|
dataset('queue_bootstrappers', [
|
||||||
|
QueueTenancyBootstrapper::class,
|
||||||
|
PersistentQueueTenancyBootstrapper::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function withQueueBootstrapper(string $class) {
|
||||||
|
config(['tenancy.bootstrappers' => [
|
||||||
|
DatabaseTenancyBootstrapper::class,
|
||||||
|
$class,
|
||||||
|
]]);
|
||||||
|
|
||||||
|
$class::__constructStatic(app());
|
||||||
|
}
|
||||||
|
|
||||||
|
test('tenant id is passed to tenant queues', function (string $bootstrapper) {
|
||||||
|
withQueueBootstrapper($bootstrapper);
|
||||||
withTenantDatabases();
|
withTenantDatabases();
|
||||||
|
|
||||||
config(['queue.default' => 'sync']);
|
config(['queue.default' => 'sync']);
|
||||||
|
|
@ -61,9 +74,10 @@ test('tenant id is passed to tenant queues', function () {
|
||||||
Event::assertDispatched(JobProcessing::class, function ($event) {
|
Event::assertDispatched(JobProcessing::class, function ($event) {
|
||||||
return $event->job->payload()['tenant_id'] === tenant('id');
|
return $event->job->payload()['tenant_id'] === tenant('id');
|
||||||
});
|
});
|
||||||
});
|
})->with('queue_bootstrappers');
|
||||||
|
|
||||||
test('tenant id is not passed to central queues', function () {
|
test('tenant id is not passed to central queues', function (string $bootstrapper) {
|
||||||
|
withQueueBootstrapper($bootstrapper);
|
||||||
withTenantDatabases();
|
withTenantDatabases();
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
|
|
@ -82,9 +96,10 @@ test('tenant id is not passed to central queues', function () {
|
||||||
Event::assertDispatched(JobProcessing::class, function ($event) {
|
Event::assertDispatched(JobProcessing::class, function ($event) {
|
||||||
return ! isset($event->job->payload()['tenant_id']);
|
return ! isset($event->job->payload()['tenant_id']);
|
||||||
});
|
});
|
||||||
});
|
})->with('queue_bootstrappers');
|
||||||
|
|
||||||
test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) {
|
test('tenancy is initialized inside queues', function (bool $shouldEndTenancy, string $bootstrapper) {
|
||||||
|
withQueueBootstrapper($bootstrapper);
|
||||||
withTenantDatabases();
|
withTenantDatabases();
|
||||||
withFailedJobs();
|
withFailedJobs();
|
||||||
|
|
||||||
|
|
@ -117,7 +132,7 @@ test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) {
|
||||||
$tenant->run(function () use ($user) {
|
$tenant->run(function () use ($user) {
|
||||||
expect($user->fresh()->name)->toBe('Bar');
|
expect($user->fresh()->name)->toBe('Bar');
|
||||||
});
|
});
|
||||||
})->with([true, false]);
|
})->with([true, false])->with('queue_bootstrappers');
|
||||||
|
|
||||||
test('changing the shouldQueue static property in parent class affects child classes unless the property is redefined', function () {
|
test('changing the shouldQueue static property in parent class affects child classes unless the property is redefined', function () {
|
||||||
// Parent – $shouldQueue is true
|
// Parent – $shouldQueue is true
|
||||||
|
|
@ -142,7 +157,8 @@ test('changing the shouldQueue static property in parent class affects child cla
|
||||||
expect(app(ShouldNotQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
|
expect(app(ShouldNotQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy) {
|
test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy, string $bootstrapper) {
|
||||||
|
withQueueBootstrapper($bootstrapper);
|
||||||
withFailedJobs();
|
withFailedJobs();
|
||||||
withTenantDatabases();
|
withTenantDatabases();
|
||||||
|
|
||||||
|
|
@ -189,9 +205,10 @@ test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenan
|
||||||
$tenant->run(function () use ($user) {
|
$tenant->run(function () use ($user) {
|
||||||
expect($user->fresh()->name)->toBe('Bar');
|
expect($user->fresh()->name)->toBe('Bar');
|
||||||
});
|
});
|
||||||
})->with([true, false]);
|
})->with([true, false])->with('queue_bootstrappers');
|
||||||
|
|
||||||
test('the tenant used by the job doesnt change when the current tenant changes', function () {
|
test('the tenant used by the job doesnt change when the current tenant changes', function (string $bootstrapper) {
|
||||||
|
withQueueBootstrapper($bootstrapper);
|
||||||
withTenantDatabases();
|
withTenantDatabases();
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
$tenant1 = Tenant::create();
|
||||||
|
|
@ -208,26 +225,11 @@ test('the tenant used by the job doesnt change when the current tenant changes',
|
||||||
pest()->artisan('queue:work --once');
|
pest()->artisan('queue:work --once');
|
||||||
|
|
||||||
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant1->getTenantKey());
|
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant1->getTenantKey());
|
||||||
});
|
})->with('queue_bootstrappers');
|
||||||
|
|
||||||
test('tenant connections do not persist after tenant jobs get processed', function() {
|
|
||||||
withTenantDatabases();
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
dispatch(new TestJob(pest()->valuestore));
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
pest()->artisan('queue:work --once');
|
|
||||||
|
|
||||||
expect(collect(DB::select('SHOW FULL PROCESSLIST'))->pluck('db'))->not()->toContain($tenant->database()->getName());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Regression test for #1277
|
// Regression test for #1277
|
||||||
test('dispatching a job from a tenant run arrow function dispatches it immediately', function () {
|
test('dispatching a job from a tenant run arrow function dispatches it immediately', function (string $bootstrapper) {
|
||||||
|
withQueueBootstrapper($bootstrapper);
|
||||||
withTenantDatabases();
|
withTenantDatabases();
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
|
|
@ -241,7 +243,7 @@ test('dispatching a job from a tenant run arrow function dispatches it immediate
|
||||||
expect(tenant())->toBe(null);
|
expect(tenant())->toBe(null);
|
||||||
|
|
||||||
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->getTenantKey());
|
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->getTenantKey());
|
||||||
});
|
})->with('queue_bootstrappers');
|
||||||
|
|
||||||
function createValueStore(): void
|
function createValueStore(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use Stancl\Tenancy\Commands\CreateUserWithRLSPolicies;
|
||||||
use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager;
|
use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager;
|
||||||
use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager;
|
use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager;
|
||||||
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
CreateUserWithRLSPolicies::$forceRls = true;
|
CreateUserWithRLSPolicies::$forceRls = true;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use Stancl\Tenancy\Commands\CreateUserWithRLSPolicies;
|
||||||
use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager;
|
use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager;
|
||||||
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
|
||||||
use Stancl\Tenancy\Database\Exceptions\RecursiveRelationshipException;
|
use Stancl\Tenancy\Database\Exceptions\RecursiveRelationshipException;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
CreateUserWithRLSPolicies::$forceRls = true;
|
CreateUserWithRLSPolicies::$forceRls = true;
|
||||||
|
|
@ -509,7 +510,7 @@ test('table rls manager generates relationship trees with tables related to the
|
||||||
|
|
||||||
// Add non-nullable comment_id foreign key
|
// Add non-nullable comment_id foreign key
|
||||||
Schema::table('ratings', function (Blueprint $table) {
|
Schema::table('ratings', function (Blueprint $table) {
|
||||||
$table->foreignId('comment_id')->onUpdate('cascade')->onDelete('cascade')->comment('rls')->constrained('comments');
|
$table->foreignId('comment_id')->comment('rls')->constrained('comments')->onUpdate('cascade')->onDelete('cascade');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Non-nullable paths are preferred over nullable paths
|
// Non-nullable paths are preferred over nullable paths
|
||||||
|
|
@ -744,16 +745,29 @@ test('table rls manager generates queries correctly', function() {
|
||||||
test('table manager throws an exception when encountering a recursive relationship', function() {
|
test('table manager throws an exception when encountering a recursive relationship', function() {
|
||||||
Schema::create('recursive_posts', function (Blueprint $table) {
|
Schema::create('recursive_posts', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('highlighted_comment_id')->constrained('comments')->nullable()->comment('rls');
|
$table->foreignId('highlighted_comment_id')->nullable()->comment('rls')->constrained('comments');
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::table('comments', function (Blueprint $table) {
|
Schema::table('comments', function (Blueprint $table) {
|
||||||
$table->foreignId('recursive_post_id')->constrained('recursive_posts')->comment('rls');
|
$table->foreignId('recursive_post_id')->comment('rls')->constrained('recursive_posts');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fn () => app(TableRLSManager::class)->generateTrees())->toThrow(RecursiveRelationshipException::class);
|
expect(fn () => app(TableRLSManager::class)->generateTrees())->toThrow(RecursiveRelationshipException::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('table manager ignores recursive relationship if the foreign key responsible for the recursion has no-rls comment', function() {
|
||||||
|
Schema::create('recursive_posts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('highlighted_comment_id')->nullable()->comment('no-rls')->constrained('comments');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('comments', function (Blueprint $table) {
|
||||||
|
$table->foreignId('recursive_post_id')->comment('rls')->constrained('recursive_posts');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fn () => app(TableRLSManager::class)->generateTrees())->not()->toThrow(RecursiveRelationshipException::class);
|
||||||
|
});
|
||||||
|
|
||||||
function createPostgresUser(string $username, string $password = 'password'): array
|
function createPostgresUser(string $username, string $password = 'password'): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ use Stancl\Tenancy\Commands\CreateUserWithRLSPolicies;
|
||||||
use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager;
|
use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager;
|
||||||
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
|
||||||
use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel;
|
use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
CreateUserWithRLSPolicies::$forceRls = true;
|
CreateUserWithRLSPolicies::$forceRls = true;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config([
|
config([
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
|
||||||
use Stancl\Tenancy\Tests\Etc\ResourceSyncing\CentralUser as BaseCentralUser;
|
use Stancl\Tenancy\Tests\Etc\ResourceSyncing\CentralUser as BaseCentralUser;
|
||||||
use Stancl\Tenancy\ResourceSyncing\CentralResourceNotAvailableInPivotException;
|
use Stancl\Tenancy\ResourceSyncing\CentralResourceNotAvailableInPivotException;
|
||||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase;
|
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config(['tenancy.bootstrappers' => [
|
config(['tenancy.bootstrappers' => [
|
||||||
|
|
|
||||||
73
tests/RunForMultipleTest.php
Normal file
73
tests/RunForMultipleTest.php
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\User;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
|
||||||
|
Event::listen(TenantCreated::class, JobPipeline::make([
|
||||||
|
CreateDatabase::class,
|
||||||
|
MigrateDatabase::class,
|
||||||
|
])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener());
|
||||||
|
|
||||||
|
config(['tenancy.bootstrappers' => [
|
||||||
|
DatabaseTenancyBootstrapper::class,
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runForMultiple runs the passed closure for the right tenants', function() {
|
||||||
|
$tenants = [Tenant::create(), Tenant::create(), Tenant::create()];
|
||||||
|
|
||||||
|
$createUser = fn ($username) => function () use ($username) {
|
||||||
|
User::create(['name' => $username, 'email' => Str::random(8) . '@example.com', 'password' => bcrypt('password')]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// tenancy()->runForMultiple([], ...) shouldn't do anything
|
||||||
|
// No users should be created -- the closure should not run at all
|
||||||
|
tenancy()->runForMultiple([], $createUser('none'));
|
||||||
|
// Try the same with an empty collection -- the result should be the same for any traversable
|
||||||
|
tenancy()->runForMultiple(collect(), $createUser('none'));
|
||||||
|
|
||||||
|
foreach ($tenants as $tenant) {
|
||||||
|
$tenant->run(function() {
|
||||||
|
expect(User::count())->toBe(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// tenancy()->runForMultiple(['foo', 'bar'], ...) should run the closure only for the passed tenants
|
||||||
|
tenancy()->runForMultiple([$tenants[0]->getTenantKey(), $tenants[1]->getTenantKey()], $createUser('user'));
|
||||||
|
|
||||||
|
// User should be created for tenants[0] and tenants[1], but not for tenants[2]
|
||||||
|
foreach ($tenants as $tenant) {
|
||||||
|
$tenant->run(function() use ($tenants) {
|
||||||
|
if (tenant()->getTenantKey() !== $tenants[2]->getTenantKey()) {
|
||||||
|
expect(User::first()->name)->toBe('user');
|
||||||
|
} else {
|
||||||
|
expect(User::count())->toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// tenancy()->runForMultiple(null, ...) should run the closure for all tenants
|
||||||
|
tenancy()->runForMultiple(null, $createUser('new_user'));
|
||||||
|
|
||||||
|
foreach ($tenants as $tenant) {
|
||||||
|
$tenant->run(function() {
|
||||||
|
expect(User::all()->pluck('name'))->toContain('new_user');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -8,6 +8,7 @@ use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||||
use Stancl\Tenancy\Middleware\ScopeSessions;
|
use Stancl\Tenancy\Middleware\ScopeSessions;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Route::group([
|
Route::group([
|
||||||
|
|
@ -54,3 +55,15 @@ test('an exception is thrown when the middleware is executed before tenancy is i
|
||||||
pest()->expectException(TenancyNotInitializedException::class);
|
pest()->expectException(TenancyNotInitializedException::class);
|
||||||
$this->withoutExceptionHandling()->get('http://acme.localhost/bar');
|
$this->withoutExceptionHandling()->get('http://acme.localhost/bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('scope sessions mw can be used on universal routes', function() {
|
||||||
|
Route::get('/universal', function () {
|
||||||
|
return true;
|
||||||
|
})->middleware(['universal', InitializeTenancyBySubdomain::class, ScopeSessions::class]);
|
||||||
|
|
||||||
|
Tenant::create([
|
||||||
|
'id' => 'acme',
|
||||||
|
])->createDomain('acme');
|
||||||
|
|
||||||
|
pest()->withoutExceptionHandling()->get('http://localhost/universal')->assertSuccessful();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
// todo@tests write similar low-level tests for the cache bootstrapper? including the database driver in a single-db setup
|
// todo@tests write similar low-level tests for the cache bootstrapper? including the database driver in a single-db setup
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
|
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
|
||||||
use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel;
|
use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel;
|
||||||
use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules;
|
use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Schema::create('posts', function (Blueprint $table) {
|
Schema::create('posts', function (Blueprint $table) {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config(['tenancy.models.tenant' => SingleDomainTenant::class]);
|
config(['tenancy.models.tenant' => SingleDomainTenant::class]);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||||
use Stancl\Tenancy\Exceptions\NotASubdomainException;
|
use Stancl\Tenancy\Exceptions\NotASubdomainException;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||||
use Stancl\Tenancy\Database\Models;
|
use Stancl\Tenancy\Database\Models;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
// Global state cleanup after some tests
|
// Global state cleanup after some tests
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use Stancl\Tenancy\Controllers\TenantAssetController;
|
||||||
use Stancl\Tenancy\Events\TenancyEnded;
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config(['tenancy.bootstrappers' => [
|
config(['tenancy.bootstrappers' => [
|
||||||
|
|
@ -65,7 +66,7 @@ test('asset can be accessed using the url returned by the tenant asset helper',
|
||||||
|
|
||||||
test('asset helper returns a link to tenant asset controller when asset url is null', function () {
|
test('asset helper returns a link to tenant asset controller when asset url is null', function () {
|
||||||
config(['app.asset_url' => null]);
|
config(['app.asset_url' => null]);
|
||||||
config(['tenancy.filesystem.asset_helper_tenancy' => true]);
|
config(['tenancy.filesystem.asset_helper_override' => true]);
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
|
@ -78,7 +79,7 @@ test('asset helper returns a link to tenant asset controller when asset url is n
|
||||||
|
|
||||||
test('asset helper returns a link to an external url when asset url is not null', function () {
|
test('asset helper returns a link to an external url when asset url is not null', function () {
|
||||||
config(['app.asset_url' => 'https://an-s3-bucket']);
|
config(['app.asset_url' => 'https://an-s3-bucket']);
|
||||||
config(['tenancy.filesystem.asset_helper_tenancy' => true]);
|
config(['tenancy.filesystem.asset_helper_override' => true]);
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
|
@ -93,7 +94,7 @@ test('asset helper works correctly with path identification', function (bool $ke
|
||||||
TenancyUrlGenerator::$prefixRouteNames = true;
|
TenancyUrlGenerator::$prefixRouteNames = true;
|
||||||
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
||||||
|
|
||||||
config(['tenancy.filesystem.asset_helper_tenancy' => true]);
|
config(['tenancy.filesystem.asset_helper_override' => true]);
|
||||||
config(['tenancy.identification.default_middleware' => InitializeTenancyByPath::class]);
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByPath::class]);
|
||||||
config(['tenancy.bootstrappers' => array_merge([UrlGeneratorBootstrapper::class], config('tenancy.bootstrappers'))]);
|
config(['tenancy.bootstrappers' => array_merge([UrlGeneratorBootstrapper::class], config('tenancy.bootstrappers'))]);
|
||||||
|
|
||||||
|
|
@ -165,7 +166,7 @@ test('asset helper tenancy can be disabled', function () {
|
||||||
|
|
||||||
config([
|
config([
|
||||||
'app.asset_url' => null,
|
'app.asset_url' => null,
|
||||||
'tenancy.filesystem.asset_helper_tenancy' => false,
|
'tenancy.filesystem.asset_helper_override' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('commands run globally are tenant aware and return valid exit code', function () {
|
test('commands run globally are tenant aware and return valid exit code', function () {
|
||||||
$tenant1 = Tenant::create();
|
$tenant1 = Tenant::create();
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMySQLData
|
||||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager;
|
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager;
|
||||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
|
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
|
||||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
|
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
SQLiteDatabaseManager::$path = null;
|
SQLiteDatabaseManager::$path = null;
|
||||||
|
|
@ -405,6 +406,42 @@ test('tenant database can be created by using the username and password from ten
|
||||||
expect($manager->databaseExists($name))->toBeTrue();
|
expect($manager->databaseExists($name))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('decrypted password can be used to connect to a tenant db while the password is saved as encrypted', function (string|null $tenantDbPassword) {
|
||||||
|
config([
|
||||||
|
'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
|
||||||
|
'tenancy.database.template_tenant_connection' => 'mysql',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener());
|
||||||
|
|
||||||
|
// Create a tenant, either with a specific password, or with a password generated by the DB manager
|
||||||
|
$tenant = TenantWithEncryptedPassword::create([
|
||||||
|
'tenancy_db_name' => $name = 'foo' . Str::random(8),
|
||||||
|
'tenancy_db_username' => 'user' . Str::random(4),
|
||||||
|
'tenancy_db_password' => $tenantDbPassword,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$decryptedPassword = $tenant->tenancy_db_password;
|
||||||
|
$encryptedPassword = $tenant->getAttributes()['tenancy_db_password']; // Password encrypted using the TenantWithEncryptedPassword model's encrypted cast
|
||||||
|
expect($decryptedPassword)->not()->toBe($encryptedPassword);
|
||||||
|
|
||||||
|
$passwordSavedInDatabase = json_decode(DB::select('SELECT data FROM tenants LIMIT 1')[0]->data)->tenancy_db_password;
|
||||||
|
expect($encryptedPassword)->toBe($passwordSavedInDatabase);
|
||||||
|
|
||||||
|
app(DatabaseManager::class)->connectToTenant($tenant);
|
||||||
|
|
||||||
|
// Check if we got connected to the tenant DB
|
||||||
|
expect(config('database.default'))->toBe('tenant');
|
||||||
|
expect(config('database.connections.tenant.database'))->toBe($name);
|
||||||
|
// Check if the decrypted password is used to connect to the tenant DB
|
||||||
|
expect(config('database.connections.tenant.password'))->toBe($decryptedPassword);
|
||||||
|
})->with([
|
||||||
|
'decrypted' . Str::random(8), // Use this password as the tenant DB password
|
||||||
|
null, // Let the DB manager generate the tenant DB password
|
||||||
|
]);
|
||||||
|
|
||||||
test('path used by sqlite manager can be customized', function () {
|
test('path used by sqlite manager can be customized', function () {
|
||||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
|
|
@ -529,3 +566,13 @@ function createUsersTable()
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TenantWithEncryptedPassword extends Tenant
|
||||||
|
{
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tenancy_db_password' => 'encrypted',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,16 @@ use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
use Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator;
|
use Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator;
|
||||||
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
||||||
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator;
|
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator;
|
||||||
|
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomIntGenerator;
|
||||||
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator;
|
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator;
|
||||||
|
use Stancl\Tenancy\UniqueIdentifierGenerators\ULIDGenerator;
|
||||||
|
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
RandomIntGenerator::$min = 0;
|
||||||
|
RandomIntGenerator::$max = PHP_INT_MAX;
|
||||||
|
});
|
||||||
|
|
||||||
test('created event is dispatched', function () {
|
test('created event is dispatched', function () {
|
||||||
Event::fake([TenantCreated::class]);
|
Event::fake([TenantCreated::class]);
|
||||||
|
|
@ -71,6 +80,20 @@ test('autoincrement ids are supported', function () {
|
||||||
expect($tenant2->id)->toBe(2);
|
expect($tenant2->id)->toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ulid ids are supported', function () {
|
||||||
|
app()->bind(UniqueIdentifierGenerator::class, ULIDGenerator::class);
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
expect($tenant1->id)->toBeString();
|
||||||
|
expect(strlen($tenant1->id))->toBe(26);
|
||||||
|
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
expect($tenant2->id)->toBeString();
|
||||||
|
expect(strlen($tenant2->id))->toBe(26);
|
||||||
|
|
||||||
|
expect($tenant2->id > $tenant1->id)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
test('hex ids are supported', function () {
|
test('hex ids are supported', function () {
|
||||||
app()->bind(UniqueIdentifierGenerator::class, RandomHexGenerator::class);
|
app()->bind(UniqueIdentifierGenerator::class, RandomHexGenerator::class);
|
||||||
|
|
||||||
|
|
@ -87,6 +110,16 @@ test('hex ids are supported', function () {
|
||||||
RandomHexGenerator::$bytes = 6; // reset
|
RandomHexGenerator::$bytes = 6; // reset
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('random ints are supported', function () {
|
||||||
|
app()->bind(UniqueIdentifierGenerator::class, RandomIntGenerator::class);
|
||||||
|
RandomIntGenerator::$min = 200;
|
||||||
|
RandomIntGenerator::$max = 1000;
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
expect($tenant1->id >= 200)->toBeTrue();
|
||||||
|
expect($tenant1->id <= 1000)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
test('random string ids are supported', function () {
|
test('random string ids are supported', function () {
|
||||||
app()->bind(UniqueIdentifierGenerator::class, RandomStringGenerator::class);
|
app()->bind(UniqueIdentifierGenerator::class, RandomStringGenerator::class);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Exceptions\StatefulGuardRequiredException;
|
use Stancl\Tenancy\Exceptions\StatefulGuardRequiredException;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
pest()->artisan('migrate', [
|
pest()->artisan('migrate', [
|
||||||
|
|
@ -88,7 +89,7 @@ test('tenant user can be impersonated on a tenant domain', function () {
|
||||||
expect(session('tenancy_impersonating'))->toBeTrue();
|
expect(session('tenancy_impersonating'))->toBeTrue();
|
||||||
|
|
||||||
// Leave impersonation
|
// Leave impersonation
|
||||||
UserImpersonation::leave();
|
UserImpersonation::stopImpersonating();
|
||||||
|
|
||||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||||
expect(session('tenancy_impersonating'))->toBeNull();
|
expect(session('tenancy_impersonating'))->toBeNull();
|
||||||
|
|
@ -134,7 +135,7 @@ test('tenant user can be impersonated on a tenant path', function () {
|
||||||
expect(session('tenancy_impersonating'))->toBeTrue();
|
expect(session('tenancy_impersonating'))->toBeTrue();
|
||||||
|
|
||||||
// Leave impersonation
|
// Leave impersonation
|
||||||
UserImpersonation::leave();
|
UserImpersonation::stopImpersonating();
|
||||||
|
|
||||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||||
expect(session('tenancy_impersonating'))->toBeNull();
|
expect(session('tenancy_impersonating'))->toBeNull();
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
{
|
{
|
||||||
|
|
@ -85,11 +87,8 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
'--realpath' => true,
|
'--realpath' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Laravel 6.x support todo@refactor clean up
|
\Illuminate\Testing\TestResponse::macro('assertContent', function ($content) {
|
||||||
$testResponse = class_exists('Illuminate\Testing\TestResponse') ? 'Illuminate\Testing\TestResponse' : 'Illuminate\Foundation\Testing\TestResponse';
|
\Illuminate\Testing\Assert::assertSame($content, $this->baseResponse->getContent());
|
||||||
$testResponse::macro('assertContent', function ($content) {
|
|
||||||
$assertClass = class_exists('Illuminate\Testing\Assert') ? 'Illuminate\Testing\Assert' : 'Illuminate\Foundation\Testing\Assert';
|
|
||||||
$assertClass::assertSame($content, $this->baseResponse->getContent());
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
});
|
});
|
||||||
|
|
@ -175,18 +174,25 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
'tenancy.models.tenant' => Tenant::class, // Use test tenant w/ DBs & domains
|
'tenancy.models.tenant' => Tenant::class, // Use test tenant w/ DBs & domains
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$app->singleton(RedisTenancyBootstrapper::class); // todo@samuel use proper approach eg config for singleton registration
|
// Since we run the TSP with no bootstrappers enabled, we need
|
||||||
$app->singleton(CacheTenancyBootstrapper::class); // todo@samuel use proper approach eg config for singleton registration
|
// to manually register bootstrappers as singletons here.
|
||||||
|
$app->singleton(RedisTenancyBootstrapper::class);
|
||||||
|
$app->singleton(CacheTenancyBootstrapper::class);
|
||||||
$app->singleton(BroadcastingConfigBootstrapper::class);
|
$app->singleton(BroadcastingConfigBootstrapper::class);
|
||||||
$app->singleton(BroadcastChannelPrefixBootstrapper::class);
|
$app->singleton(BroadcastChannelPrefixBootstrapper::class);
|
||||||
$app->singleton(PostgresRLSBootstrapper::class);
|
$app->singleton(PostgresRLSBootstrapper::class);
|
||||||
$app->singleton(MailConfigBootstrapper::class);
|
$app->singleton(MailConfigBootstrapper::class);
|
||||||
$app->singleton(RootUrlBootstrapper::class);
|
$app->singleton(RootUrlBootstrapper::class);
|
||||||
$app->singleton(UrlGeneratorBootstrapper::class);
|
$app->singleton(UrlGeneratorBootstrapper::class);
|
||||||
|
$app->singleton(FilesystemTenancyBootstrapper::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getPackageProviders($app)
|
protected function getPackageProviders($app)
|
||||||
{
|
{
|
||||||
|
TenancyServiceProvider::$configure = function () {
|
||||||
|
config(['tenancy.bootstrappers' => []]);
|
||||||
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
TenancyServiceProvider::class,
|
TenancyServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Stancl\Tenancy\Enums\RouteMode;
|
use Stancl\Tenancy\Enums\RouteMode;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
use Illuminate\Contracts\Http\Kernel;
|
use Illuminate\Contracts\Http\Kernel;
|
||||||
|
|
@ -11,16 +9,14 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||||
use Stancl\Tenancy\Tests\Etc\HasMiddlewareController;
|
use Stancl\Tenancy\Tests\Etc\HasMiddlewareController;
|
||||||
use Stancl\Tenancy\Middleware\IdentificationMiddleware;
|
|
||||||
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
|
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||||||
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
|
||||||
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
|
||||||
test('a route can be universal using domain identification', function (array $routeMiddleware, array $globalMiddleware) {
|
test('a route can be universal using domain identification', function (array $routeMiddleware, array $globalMiddleware) {
|
||||||
foreach ($globalMiddleware as $middleware) {
|
foreach ($globalMiddleware as $middleware) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue