Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
matrix:
include:
- laravel: "^12.0"
- laravel: "^13.0"

steps:
- name: Checkout
Expand Down
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
"require": {
"php": "^8.4",
"ext-json": "*",
"illuminate/support": "^12.0",
"laravel/tinker": "^2.0",
"illuminate/support": "^12.0|^13.0",
"laravel/tinker": "^2.0|^3.0",
"ramsey/uuid": "^4.7.3",
"stancl/jobpipeline": "2.0.0-rc6",
"stancl/jobpipeline": "2.0.0-rc7",
"stancl/virtualcolumn": "^1.5.0",
"spatie/invade": "*",
"laravel/prompts": "0.*"
},
"require-dev": {
"laravel/framework": "^12.0",
"orchestra/testbench": "^10.0",
"laravel/framework": "^13.0",
"orchestra/testbench": "^10.0|^11.0",
"league/flysystem-aws-s3-v3": "^3.12.2",
"doctrine/dbal": "^3.6.0",
"spatie/valuestore": "^1.2.5",
Expand Down
11 changes: 11 additions & 0 deletions src/Database/Concerns/BelongsToPrimaryModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ trait BelongsToPrimaryModel
abstract public function getRelationshipToPrimaryModel(): string;

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;

Expand Down
11 changes: 11 additions & 0 deletions src/Database/Concerns/BelongsToTenant.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ public function tenant(): BelongsTo
}

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
// Postgres RLS is used for scoping, so we don't enable the scope used with single-database tenancy.
Expand Down
37 changes: 24 additions & 13 deletions tests/SessionSeparationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
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
Expand Down Expand Up @@ -100,7 +101,7 @@
expect($redisClient->getOption($redisClient::OPT_PREFIX) === "tenant_{$tenant->id}_")->toBe($bootstrappedEnabled);

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);
})->with([true, false]);

Expand All @@ -118,13 +119,13 @@
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
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();
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) {
return str($key)->startsWith("foolaravel_cache_tenant_{$tenant->id}");
return str($key)->startsWith(formatLaravelCacheKey(prefix: 'foo', suffix: "tenant_{$tenant->id}"));
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);

Expand All @@ -148,14 +149,14 @@
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
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();
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
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);

Artisan::call('cache:clear memcached');
Expand All @@ -177,13 +178,13 @@
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
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();
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) {
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}"));
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);

Expand All @@ -202,13 +203,13 @@
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
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();
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) {
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}"));
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);

Expand Down Expand Up @@ -250,3 +251,13 @@
// [false, true], // when the connection IS set, the session bootstrapper becomes necessary
[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