From 483a3ec802e14521fd16c69b2a14476adc3fef51 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 30 Jul 2026 15:57:40 +0200 Subject: [PATCH] Fix scopeCache() discarding configured cache store paths scopeCache() rewrote path and lock_path for every file-driver store to a hardcoded '/framework/cache/data' path, completely ignoring the store's config. Now, scopeCache() remembers each store's original path and lock_path, scopes these paths for the tenant, and restores them to the stored originals on revert. The store's lock_path was always overwritten by the same hardcoded path. But lock_path is configurable too, AND it's actually optional (unlike path). If it's not configured at all (= it's null or just unset), Laravel automatically falls back to the store's path. So in that case, leave lock_path null instead of assigning the path to it. This is not a *huge* change, assigning path to lock_path would essentially achieve the same thing, BUT if someone explicitly sets lock_path to null in the config, we should just respect that and let Laravel fall back to the path instead of setting the lock_path ourselves. Also, on revert(), the same hardcoded path was used in scopeCache(). So if someone used a custom file driver-based store, cached something in central context, initialized and ended tenancy, the central cache got corrupt (see the 'central cache is not lost when tenancy ends' test). --- .../FilesystemTenancyBootstrapper.php | 70 +++++++++++++++++-- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index 0a864fbd..c573db8f 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -14,6 +14,21 @@ use Stancl\Tenancy\Contracts\Tenant; class FilesystemTenancyBootstrapper implements TenancyBootstrapper { public array $originalDisks = []; + + /** + * The path and lock_path each file cache store had in the central context, keyed by store name. + * + * For example: + * [ + * 'file' => [ + * 'path' => storage_path('framework/cache/data'), + * 'lock_path' => storage_path('framework/cache/data'), + * ], + * ] + * + * Used to scope the store to a tenant, and to restore it back to this when tenancy ends. + */ + protected array $originalCachePaths = []; public string|null $originalAssetUrl; public string $originalStoragePath; @@ -191,10 +206,6 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper return; } - $storagePath = $suffix - ? $this->tenantStoragePath($suffix) - : $this->originalStoragePath; - $stores = array_filter($this->app['config']['tenancy.cache.stores'], function ($name) { $store = $this->app['config']["cache.stores.{$name}"]; @@ -206,17 +217,62 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper }); foreach ($stores as $name) { - $path = $storagePath . '/framework/cache/data'; + // Store the original configured cache paths, unless they're already stored -- + // scopeCache() is also called in revert(), by which point the originals were + // already captured in bootstrap(). + $this->originalCachePaths[$name] ??= [ + 'path' => $this->app['config']["cache.stores.{$name}.path"], + 'lock_path' => $this->app['config']["cache.stores.{$name}.lock_path"], + ]; + + $path = $this->scopeCachePath($this->originalCachePaths[$name]['path'], $suffix); + $lockPath = $this->originalCachePaths[$name]['lock_path']; + if ($lockPath !== null) { + // Unlike path, lock_path is optional -- if it's not set, FileStore::lock() falls + // back to path itself (see `$this->lockDirectory ?? $this->directory` in FileStore). + // Leave it null here rather than hardcoding it to $path ourselves, so a store that didn't + // configure a separate lock_path doesn't end up with one. + $lockPath = $this->scopeCachePath($lockPath, $suffix); + } + $this->app['config']["cache.stores.{$name}.path"] = $path; - $this->app['config']["cache.stores.{$name}.lock_path"] = $path; + $this->app['config']["cache.stores.{$name}.lock_path"] = $lockPath; /** @var \Illuminate\Cache\FileStore $store */ $store = $this->app['cache']->store($name)->getStore(); $store->setDirectory($path); - $store->setLockDirectory($path); + $store->setLockDirectory($lockPath); } } + /** + * Return a configured path ($configuredPath is e.g. storage_path('framework/cache/data')), + * scoped to the current tenant when called from bootstrap(), + * or unchanged when called from revert() (= when $suffix is false). + */ + protected function scopeCachePath(string $configuredPath, string|false $suffix): string + { + if ($suffix === false) { + return $configuredPath; + } + + if (str_starts_with($configuredPath, $this->originalStoragePath . '/')) { + // Swap the central storage path prefix for the tenant's. + // For example, storage_path('framework/cache/data') becomes storage_path('tenant1/framework/cache/data'). + $scopedPath = str($configuredPath) + ->after($this->originalStoragePath . '/') + ->prepend($this->tenantStoragePath($suffix) . '/') + ->toString(); + } else { + // Append the tenant suffix so tenants don't share the same cache directory. $configuredPath + // isn't guaranteed to be storage_path()-based (e.g. it could point to a shared network + // mount used to keep the file cache off each server's local disk in a multi-server setup). + $scopedPath = rtrim($configuredPath, '/') . '/' . $suffix; + } + + return $scopedPath; + } + public function scopeSessions(string|false $suffix): void { if (! $this->app['config']['tenancy.filesystem.scope_sessions']) {