mirror of
https://github.com/archtechx/tenancy.git
synced 2026-03-21 23:24:03 +00:00
[4.x] Laravel 13 support (#1443)
- Update ci.yml and composer.json - Wrap single database tenancy trait scopes in whenBooted() - Update SessionSeparationTest to use laravel-cache- prefix in L13 and laravel_cache_ in <=L12. Our own prefix remains tenant_%tenant%_ (as configured in tenancy.cache.prefix). We could update this to be tenant-%tenant%- from now on for consistency with Laravel's prefixes (changed in https://github.com/laravel/framework/pull/56172) but I'm not sure yet. _ seems to read a bit better but perhaps consistency is more important. We may change this later and it can be adjusted in userland easily (since it's just a config option).
This commit is contained in:
parent
8f3ea6297f
commit
c4960b76cb
5 changed files with 52 additions and 18 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -17,6 +17,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- laravel: "^12.0"
|
- laravel: "^12.0"
|
||||||
|
- laravel: "^13.0"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,17 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4",
|
"php": "^8.4",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"illuminate/support": "^12.0",
|
"illuminate/support": "^12.0|^13.0",
|
||||||
"laravel/tinker": "^2.0",
|
"laravel/tinker": "^2.0|^3.0",
|
||||||
"ramsey/uuid": "^4.7.3",
|
"ramsey/uuid": "^4.7.3",
|
||||||
"stancl/jobpipeline": "2.0.0-rc6",
|
"stancl/jobpipeline": "2.0.0-rc7",
|
||||||
"stancl/virtualcolumn": "^1.5.0",
|
"stancl/virtualcolumn": "^1.5.0",
|
||||||
"spatie/invade": "*",
|
"spatie/invade": "*",
|
||||||
"laravel/prompts": "0.*"
|
"laravel/prompts": "0.*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^13.0",
|
||||||
"orchestra/testbench": "^10.0",
|
"orchestra/testbench": "^10.0|^11.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",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,17 @@ trait BelongsToPrimaryModel
|
||||||
abstract public function getRelationshipToPrimaryModel(): string;
|
abstract public function getRelationshipToPrimaryModel(): string;
|
||||||
|
|
||||||
public static function bootBelongsToPrimaryModel(): void
|
public static function bootBelongsToPrimaryModel(): void
|
||||||
|
{
|
||||||
|
if (method_exists(static::class, 'whenBooted')) {
|
||||||
|
// Laravel 13
|
||||||
|
// For context see https://github.com/calebporzio/sushi/commit/62ff7f432cac736cb1da9f46d8f471cb78914b92
|
||||||
|
static::whenBooted(fn () => static::configureBelongsToPrimaryModelScope());
|
||||||
|
} else {
|
||||||
|
static::configureBelongsToPrimaryModelScope();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function configureBelongsToPrimaryModelScope()
|
||||||
{
|
{
|
||||||
$implicitRLS = config('tenancy.rls.manager') === TraitRLSManager::class && TraitRLSManager::$implicitRLS;
|
$implicitRLS = config('tenancy.rls.manager') === TraitRLSManager::class && TraitRLSManager::$implicitRLS;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,17 @@ trait BelongsToTenant
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function bootBelongsToTenant(): void
|
public static function bootBelongsToTenant(): void
|
||||||
|
{
|
||||||
|
if (method_exists(static::class, 'whenBooted')) {
|
||||||
|
// Laravel 13
|
||||||
|
// For context see https://github.com/calebporzio/sushi/commit/62ff7f432cac736cb1da9f46d8f471cb78914b92
|
||||||
|
static::whenBooted(fn () => static::configureBelongsToTenantScope());
|
||||||
|
} else {
|
||||||
|
static::configureBelongsToTenantScope();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function configureBelongsToTenantScope(): void
|
||||||
{
|
{
|
||||||
// If TraitRLSManager::$implicitRLS is true or this model implements RLSModel
|
// If TraitRLSManager::$implicitRLS is true or this model implements RLSModel
|
||||||
// Postgres RLS is used for scoping, so we don't enable the scope used with single-database tenancy.
|
// Postgres RLS is used for scoping, so we don't enable the scope used with single-database tenancy.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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
|
||||||
|
|
@ -100,7 +101,7 @@ test('redis sessions are separated using the redis bootstrapper', function (bool
|
||||||
expect($redisClient->getOption($redisClient::OPT_PREFIX) === "tenant_{$tenant->id}_")->toBe($bootstrappedEnabled);
|
expect($redisClient->getOption($redisClient::OPT_PREFIX) === "tenant_{$tenant->id}_")->toBe($bootstrappedEnabled);
|
||||||
|
|
||||||
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
|
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
|
||||||
return str($key)->startsWith("tenant_{$tenant->id}_laravel_cache_");
|
return str($key)->startsWith(formatLaravelCacheKey(prefix: "tenant_{$tenant->id}_"));
|
||||||
}))->toHaveCount($bootstrappedEnabled ? 1 : 0);
|
}))->toHaveCount($bootstrappedEnabled ? 1 : 0);
|
||||||
})->with([true, false]);
|
})->with([true, false]);
|
||||||
|
|
||||||
|
|
@ -118,13 +119,13 @@ test('redis sessions are separated using the cache bootstrapper', function (bool
|
||||||
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
pest()->get("/{$tenant->id}/foo");
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions);
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey());
|
||||||
|
|
||||||
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
|
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
|
||||||
return str($key)->startsWith("foolaravel_cache_tenant_{$tenant->id}");
|
return str($key)->startsWith(formatLaravelCacheKey(prefix: 'foo', suffix: "tenant_{$tenant->id}"));
|
||||||
}))->toHaveCount($scopeSessions ? 1 : 0);
|
}))->toHaveCount($scopeSessions ? 1 : 0);
|
||||||
})->with([true, false]);
|
})->with([true, false]);
|
||||||
|
|
||||||
|
|
@ -148,14 +149,14 @@ test('memcached sessions are separated using the cache bootstrapper', function (
|
||||||
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
pest()->get("/{$tenant->id}/foo");
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions);
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey());
|
||||||
|
|
||||||
sleep(1.1); // 1s+ sleep is necessary for getAllKeys() to work. if this causes race conditions or we want to avoid the delay, we can refactor this to some type of a mock
|
sleep(1.1); // 1s+ sleep is necessary for getAllKeys() to work. if this causes race conditions or we want to avoid the delay, we can refactor this to some type of a mock
|
||||||
expect(array_filter($allMemcachedKeys(), function (string $key) use ($tenant) {
|
expect(array_filter($allMemcachedKeys(), function (string $key) use ($tenant) {
|
||||||
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
|
return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}"));
|
||||||
}))->toHaveCount($scopeSessions ? 1 : 0);
|
}))->toHaveCount($scopeSessions ? 1 : 0);
|
||||||
|
|
||||||
Artisan::call('cache:clear memcached');
|
Artisan::call('cache:clear memcached');
|
||||||
|
|
@ -177,13 +178,13 @@ test('dynamodb sessions are separated using the cache bootstrapper', function (b
|
||||||
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
pest()->get("/{$tenant->id}/foo");
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions);
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey());
|
||||||
|
|
||||||
expect(array_filter($allDynamodbKeys(), function (string $key) use ($tenant) {
|
expect(array_filter($allDynamodbKeys(), function (string $key) use ($tenant) {
|
||||||
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
|
return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}"));
|
||||||
}))->toHaveCount($scopeSessions ? 1 : 0);
|
}))->toHaveCount($scopeSessions ? 1 : 0);
|
||||||
})->with([true, false]);
|
})->with([true, false]);
|
||||||
|
|
||||||
|
|
@ -202,13 +203,13 @@ test('apc sessions are separated using the cache bootstrapper', function (bool $
|
||||||
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
pest()->get("/{$tenant->id}/foo");
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions);
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey());
|
||||||
|
|
||||||
expect(array_filter($allApcuKeys(), function (string $key) use ($tenant) {
|
expect(array_filter($allApcuKeys(), function (string $key) use ($tenant) {
|
||||||
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
|
return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}"));
|
||||||
}))->toHaveCount($scopeSessions ? 1 : 0);
|
}))->toHaveCount($scopeSessions ? 1 : 0);
|
||||||
})->with([true, false]);
|
})->with([true, false]);
|
||||||
|
|
||||||
|
|
@ -250,3 +251,13 @@ test('database sessions are separated regardless of whether the session bootstra
|
||||||
// [false, true], // when the connection IS set, the session bootstrapper becomes necessary
|
// [false, true], // when the connection IS set, the session bootstrapper becomes necessary
|
||||||
[false, false],
|
[false, false],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function formatLaravelCacheKey(string $suffix = '', string $prefix = ''): string
|
||||||
|
{
|
||||||
|
// todo@release if we drop Laravel 12 support we can just switch to - syntax everywhere
|
||||||
|
if (version_compare(app()->version(), '13.0.0') >= 0) {
|
||||||
|
return $prefix . 'laravel-cache-' . $suffix;
|
||||||
|
} else {
|
||||||
|
return $prefix . 'laravel_cache_' . $suffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue