1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-13 00:14:04 +00:00

Merge branch 'master' into resource-syncing-refactor

This commit is contained in:
lukinovec 2025-11-05 16:06:49 +01:00 committed by GitHub
commit 728d2db321
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 99 additions and 23 deletions

View file

@ -48,6 +48,8 @@ return [
* SECURITY NOTE: Keep in mind that autoincrement IDs come with potential enumeration issues (such as tenant storage URLs).
*
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\ULIDGenerator
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\UUIDv7Generator
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomIntGenerator
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator
@ -311,7 +313,7 @@ return [
*
* Note: This will implicitly add your configured session store to the list of prefixed stores above.
*/
'scope_sessions' => true,
'scope_sessions' => in_array(env('SESSION_DRIVER'), ['redis', 'memcached', 'dynamodb', 'apc'], true),
'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call.
],

View file

@ -102,14 +102,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
if ($this->config->get('tenancy.cache.scope_sessions', true)) {
// These are the only cache driven session backends (see Laravel's config/session.php)
if (! in_array($this->config->get('session.driver'), ['redis', 'memcached', 'dynamodb', 'apc'], true)) {
if (app()->environment('production')) {
// We only throw this exception in prod to make configuration a little easier. Developers
// may have scope_sessions set to true while using different session drivers e.g. in tests.
// Previously we just silently ignored this, however since session scoping is of high importance
// in production, we make sure to notify the developer, by throwing an exception, that session
// scoping isn't happening as expected/configured due to an incompatible session driver.
throw new Exception('Session driver [' . $this->config->get('session.driver') . '] cannot be scoped by tenancy.cache.scope_session');
}
throw new Exception('Session driver [' . $this->config->get('session.driver') . '] cannot be scoped by tenancy.cache.scope_sessions');
} else {
// Scoping sessions using this bootstrapper implicitly adds the session store to $names
$names[] = $this->getSessionCacheStoreName();

View file

@ -63,13 +63,17 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper
$stores = $this->scopedStoreNames();
foreach ($stores as $storeName) {
$this->originalConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.connection");
$this->originalLockConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.lock_connection");
$this->originalConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.connection") ?? config('tenancy.database.central_connection');
$this->originalLockConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.lock_connection") ?? config('tenancy.database.central_connection');
$this->config->set("cache.stores.{$storeName}.connection", 'tenant');
$this->config->set("cache.stores.{$storeName}.lock_connection", 'tenant');
$this->cache->purge($storeName);
/** @var DatabaseStore $store */
$store = $this->cache->store($storeName)->getStore();
$store->setConnection(DB::connection('tenant'));
$store->setLockConnection(DB::connection('tenant'));
}
if (static::$adjustGlobalCacheManager) {
@ -78,8 +82,8 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper
// *from here* being executed repeatedly in a loop on reinitialization. For that reason we do not do that
// (this is our only use of $adjustCacheManagerUsing anyway) but ideally at some point we'd have a better solution.
$originalConnections = array_combine($stores, array_map(fn (string $storeName) => [
'connection' => $this->originalConnections[$storeName] ?? config('tenancy.database.central_connection'),
'lockConnection' => $this->originalLockConnections[$storeName] ?? config('tenancy.database.central_connection'),
'connection' => $this->originalConnections[$storeName],
'lockConnection' => $this->originalLockConnections[$storeName],
], $stores));
TenancyServiceProvider::$adjustCacheManagerUsing = static function (CacheManager $manager) use ($originalConnections) {
@ -100,7 +104,11 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper
$this->config->set("cache.stores.{$storeName}.connection", $originalConnection);
$this->config->set("cache.stores.{$storeName}.lock_connection", $this->originalLockConnections[$storeName]);
$this->cache->purge($storeName);
/** @var DatabaseStore $store */
$store = $this->cache->store($storeName)->getStore();
$store->setConnection(DB::connection($this->originalConnections[$storeName]));
$store->setLockConnection(DB::connection($this->originalLockConnections[$storeName]));
}
TenancyServiceProvider::$adjustCacheManagerUsing = null;

View file

@ -78,6 +78,15 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
return;
}
$path = $suffix
? $this->tenantStoragePath($suffix) . '/framework/cache'
: $this->originalStoragePath . '/framework/cache';
if (! is_dir($path)) {
// Create tenant framework/cache directory if it does not exist
mkdir($path, 0750, true);
}
if ($suffix === false) {
$this->app->useStoragePath($this->originalStoragePath);
} else {
@ -211,7 +220,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
if (! is_dir($path)) {
// Create tenant framework/sessions directory if it does not exist
mkdir($path, 0755, true);
mkdir($path, 0750, true);
}
$this->app['config']['session.files'] = $path;

View file

@ -4,18 +4,25 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Listeners;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Events\Contracts\TenantEvent;
/**
* Can be used to manually create framework directories in the tenant storage when storage_path() is scoped.
*
* Useful when using real-time facades which use the framework/cache directory.
*
* Generally not needed anymore as the directory is also created by the FilesystemTenancyBootstrapper.
*/
class CreateTenantStorage
{
public function handle(TenantCreated $event): void
public function handle(TenantEvent $event): void
{
$storage_path = tenancy()->run($event->tenant, fn () => storage_path());
$cache_path = "$storage_path/framework/cache";
if (! is_dir($cache_path)) {
// Create the tenant's storage directory and /framework/cache within (used for e.g. real-time facades)
mkdir($cache_path, 0777, true);
mkdir($cache_path, 0750, true);
}
}
}

View file

@ -5,11 +5,11 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Listeners;
use Illuminate\Support\Facades\File;
use Stancl\Tenancy\Events\DeletingTenant;
use Stancl\Tenancy\Events\Contracts\TenantEvent;
class DeleteTenantStorage
{
public function handle(DeletingTenant $event): void
public function handle(TenantEvent $event): void
{
$path = tenancy()->run($event->tenant, fn () => storage_path());

View file

@ -9,7 +9,7 @@ use Illuminate\Support\Str;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
/**
* Generates a UUID for the tenant key.
* Generates a ULID for the tenant key.
*/
class ULIDGenerator implements UniqueIdentifierGenerator
{

View file

@ -9,7 +9,7 @@ use Ramsey\Uuid\Uuid;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
/**
* Generates a UUID for the tenant key.
* Generates a UUIDv4 for the tenant key.
*/
class UUIDGenerator implements UniqueIdentifierGenerator
{

View 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 UUIDv7 for the tenant key.
*/
class UUIDv7Generator implements UniqueIdentifierGenerator
{
public static function generate(Model $model): string|int
{
return Str::uuid7()->toString();
}
}

View file

@ -200,3 +200,24 @@ test('tenant storage can get deleted after the tenant when DeletingTenant listen
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
});
test('the framework/cache directory is created when storage_path is scoped', function (bool $suffixStoragePath) {
config([
'tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class,
],
'tenancy.filesystem.suffix_storage_path' => $suffixStoragePath
]);
$centralStoragePath = storage_path();
tenancy()->initialize($tenant = Tenant::create());
if ($suffixStoragePath) {
expect(storage_path('framework/cache'))->toBe($centralStoragePath . "/tenant{$tenant->id}/framework/cache");
expect(is_dir($centralStoragePath . "/tenant{$tenant->id}/framework/cache"))->toBeTrue();
} else {
expect(storage_path('framework/cache'))->toBe($centralStoragePath . '/framework/cache');
expect(is_dir($centralStoragePath . "/tenant{$tenant->id}/framework/cache"))->toBeFalse();
}
})->with([true, false]);

View file

@ -56,6 +56,7 @@ test('file sessions are separated', function (bool $scopeSessions) {
if ($scopeSessions) {
expect($sessionPath())->toBe(storage_path('tenant' . $tenant->getTenantKey() . '/framework/sessions'));
expect(is_dir(storage_path('tenant' . $tenant->getTenantKey() . '/framework/sessions')))->toBeTrue();
} else {
expect($sessionPath())->toBe(storage_path('framework/sessions'));
}

View file

@ -23,6 +23,7 @@ use Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator;
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomIntGenerator;
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator;
use Stancl\Tenancy\UniqueIdentifierGenerators\ULIDGenerator;
use Stancl\Tenancy\UniqueIdentifierGenerators\UUIDv7Generator;
use function Stancl\Tenancy\Tests\pest;
@ -94,6 +95,20 @@ test('ulid ids are supported', function () {
expect($tenant2->id > $tenant1->id)->toBeTrue();
});
test('uuidv7 ids are supported', function () {
app()->bind(UniqueIdentifierGenerator::class, UUIDv7Generator::class);
$tenant1 = Tenant::create();
expect($tenant1->id)->toBeString();
expect(strlen($tenant1->id))->toBe(36);
$tenant2 = Tenant::create();
expect($tenant2->id)->toBeString();
expect(strlen($tenant2->id))->toBe(36);
expect($tenant2->id > $tenant1->id)->toBeTrue();
});
test('hex ids are supported', function () {
app()->bind(UniqueIdentifierGenerator::class, RandomHexGenerator::class);