Skip to content

[MINOR BC] [4.x] Fix FilesystemTenancyBootstrapper discarding configured cache and session paths - #1473

Open
lukinovec wants to merge 16 commits into
masterfrom
scope-cache-fix
Open

[MINOR BC] [4.x] Fix FilesystemTenancyBootstrapper discarding configured cache and session paths#1473
lukinovec wants to merge 16 commits into
masterfrom
scope-cache-fix

Conversation

@lukinovec

@lukinovec lukinovec commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The cache part of this is specific to file-driver stores that are listed in tenancy.cache.stores, while tenancy.filesystem.scope_cache is set to true. The session part applies to the file session driver, while tenancy.filesystem.scope_sessions is set to true.

Ran into this while checking whether we could drop the separate 'parallel' cache store from our boilerplate's testing setup and instead just give the 'file' store a per-process path (framework/cache/data_<parallel testing token>), so each test process gets its own cache directory. Turns out scopeCache() discards configured paths entirely, so that has no effect (see below).

Using a different directory for the file cache store by setting cache.stores.file.path (either using config([...]), or directly in config/cache.php -- doesn't matter) has no effect -- FilesystemTenancyBootstrapper::scopeCache() ignores path/lock_path entirely and rewrites both to a hardcoded <storage>/framework/cache/data path on every tenancy()->initialize()/tenancy()->end():

// In `FilesystemTenancyBootstrapper::scopeCache()` (called both in `bootstrap()` and in `revert()`)
foreach ($stores as $name) {
    $path = $storagePath . '/framework/cache/data';
    $this->app['config']["cache.stores.{$name}.path"] = $path;
    $this->app['config']["cache.stores.{$name}.lock_path"] = $path;
    ...
}

Specific issues with hardcoding the path like this:

  • a store with a configured (non-default) path gets scoped to use the default one (what I described above)
  • lock_path is always overwritten with path, so a store with a separate lock directory loses that separation
  • revert() runs the same code, so it doesn't restore what the store was configured with before tenancy initialized -- it just re-applies the same hardcoded default. Central cache ends up using the wrong path after ending tenancy.

scopeSessions() has the same bug. It never reads session.files, it hardcodes <storage>/framework/sessions on both bootstrap and revert. So a configured session path gets discarded when tenancy initializes, and it doesn't get reverted back to what it was when tenancy ends. For example, with session.files set to /tmp/foo-sessions:

In tenant context:    session.files = .../storage/tenant<key>/framework/sessions
After ending tenancy: session.files = .../storage/framework/sessions

So after ending tenancy, sessions don't go back to the configured /tmp/foo-sessions. They use the hardcoded <storage>/framework/sessions path, which was never configured anywhere.

The fix

In bootstrap(), scopeCache() captures the original configured paths and scopes those instead of using a hardcoded default.

  • If the configured path is under the central storage path, that central part gets swapped for the tenant's storage path, keeping everything after it the same (e.g. storage/framework/cache/data becomes storage/tenant1/framework/cache/data).
  • If the path isn't storage_path()-based, there's nothing to swap, so the tenant's suffix just gets appended to the end of the path instead.

On revert(), scopeCache(false) puts the captured paths back into cache.stores.{$name}.path/lock_path and into the resolved store instance, so central cache uses the path it was configured with again.

The paths are captured just once, during scopeCache() at bootstrap(). If bootstrap() fails after scopeCache() (e.g. when scopeSessions() can't create its directory), revert() never runs and the config is left with the scoped paths, so capturing a second time would lose the central ones. Also, revert() iterates the stores whose paths were captured during bootstrap rather than config('tenancy.cache.stores'). Removing a store from that config in tenant context would otherwise make revert() skip it and leave it stuck with a tenant-scoped path, and adding one would make tenancy()->end() throw because its path was never captured (see the 'scopeCache ignores changes to tenancy.cache.stores made in tenant context' test). These are edge cases most users wouldn't notice, but still, worth mentioning.

lock_path stays null when a store doesn't configure it, rather than us making it default to the scoped path -- FileStore already falls back to path for locks in that case, so we can just respect the store's original config.

scopeSessions() does the same for session.files. It captures the configured path during bootstrap() (once, for the same reason as above), scopes it, and puts it back on revert() (the default storage/framework/sessions still ends up as storage/tenant1/framework/sessions, so nothing changes for the default config).

Also added tests that cover each of the issues above (+ a test for handling paths that aren't storage_path()-based, and one for a custom session.files path).

POSSIBLE MINOR BC: Someone with 'path' => '/var/cache/foobar' currently gets tenant cache in storage/tenant1/framework/cache/data. After this fix, they get /var/cache/foobar/tenant1, so whatever is already cached in the old directory is orphaned. The same applies to a non-default storage_path()-based path, e.g. 'path' => storage_path('framework/cache/data_' . env('TEST_TOKEN', 'default')). The config is now respected while scoping. The same goes for sessions -- with a non-default session.files, tenant sessions move from storage/tenant1/framework/sessions to the configured path scoped for the tenant, so the sessions in the old directory are orphaned.

Possible further improvement (not worth pursuing, keeping this here for future reference)

Rather than tenantScopedPath() hardcoding how a store's path gets scoped, that could go through config, the same way diskRoot() resolves root_override templates. Something like:

// config/tenancy.php
'cache' => [
    'path_override' => [
        'file' => '%configured_path%/%suffix%',
    ],
],

with %configured_path%, %suffix%, %storage_path% and %original_storage_path% placeholders. Stores without an entry would keep the behavior described above (no template involved).

What that would let people do. Here's the default file store from config/cache.php:

'file' => [
    'driver' => 'file',
    'path' => storage_path('framework/cache/data'),
    'lock_path' => storage_path('framework/cache/data'),
],

The changes in this PR scope that path to storage/tenant1/framework/cache/data, so each tenant's cache directory sits next to their files:

storage/
├── framework/cache/data/      // central cache
├── tenant1/
│   ├── app/
│   └── framework/cache/data/
└── tenant2/
    ├── app/
    └── framework/cache/data/

With 'file' => '%configured_path%/%suffix%', the cache directory would stay where it's configured and each tenant would get a subdirectory in it:

storage/
├── framework/cache/data/
│   ├── tenant1/
│   └── tenant2/
├── tenant1/
│   └── app/
└── tenant2/
    └── app/

All tenant cache directories in one place, so anything that only concerns cache (skipping it in backups, deleting old files) is one path instead of one per tenant. And apparently, it makes mounting possible -- you can't mount tmpfs on storage/tenant3/framework/cache/data before tenant3 exists, but you can mount it once on storage/framework/cache/data and every tenant's cache ends up inside the mount.

But you can already get the same thing without a new config key. If the store's path is outside storage_path(), the changes in this PR append the suffix to it automatically:

'path' => '/var/cache/myapp',   // in tenant1's context, this would become '/var/cache/myapp/tenant1'

So path_override would only really add one thing -- the same structure while the cache stays inside storage/. That does matter to someone whose deploy scripts, .gitignore and volume mounts all assume storage/, but that's about it.

So I don't think we should add it. AFAIK, nobody has ever asked for a configurable cache path (I searched the issues and PRs and couldn't find anything that would suggest that this is something people want). There are also two problems with the idea itself:

  • path and lock_path would go through the same template, so '%storage_path%/framework/cache/data' brings back the first two bugs from the list above -- the configured path gets thrown away, and both directories end up in the same place. We'd have to require the template to contain %configured_path%.
  • The placeholders wouldn't match root_override, where the tenant part is %tenant% (the tenant key). Here it'd be %suffix% (suffix_base + key), so two names for nearly the same thing in one class. I think that could be confusing.

(Note that until recently I thought this would be the fix for the suffix_storage_path problem below, but I don't think that anymore -- that one should be fixed by default, not by a config key.)

More issues found while looking into this

All of this is pre-existing and untouched by this PR, so I'd deal with these in separate PRs.

EDIT: suffix_storage_path not being respected by the scoping methods is not a problem -- suffix_storage_path is supposed to affect only the storage_path() helper. I still think we should at least make that clear in the docs if possible. The paragraph about the directories not being cleaned up still holds, so just note that this is a thing we should take into account.

suffix_storage_path => false isn't respected by scopeCache() or scopeSessions(). With the setting off, in the tenant context, right after tenancy()->initialize() and before writing anything:

// respects suffix_storage_path
storage_path()    .../storage

// doesn't -- and the directory is already created
session.files     .../storage/tenant<key>/framework/sessions 

Cache only points the store at the suffixed path and lets FileStore create it on first write. scopeSessions() is worse because it calls mkdir() on every tenancy init, with no suffix_storage_path check anywhere, whether or not a session is ever written.

Also, nothing cleans those directories up. DeleteTenantStorage returns early when suffix_storage_path === false (see the DeleteTenantStorage job). The early return there is correct -- without it the next guard would catch the case anyway, since storage_path() in the tenant context is the central path there, and deleting that would be very bad of course. But it means the cache/session directories under storage/tenant<key>/ can't be reached by any cleanup at the moment (so one such directory for every tenant that ever existed). Laravel doesn't do anything with these file sessions either, since it only clears whatever session.files currently points at.

Turning scope_cache/scope_sessions off wouldn't be a feasible "solution" because for the file driver, path scoping is the only isolation there is (FileStore::getPrefix() returns a hardcoded ''), so all tenants would share one cache directory. The settings are also about different things -- suffix_storage_path is about whether storage_path() itself moves, scope_cache/scope_sessions about whether tenant cache and sessions stay separate. A central storage_path() with isolated cache sounds like a sensible combination (and I see that's what people actually want -- #196). And since suffix_storage_path, scope_cache and scope_sessions are all enabled by default in the config, someone who only sets suffix_storage_path to false (which is what the config comment tells you to do on S3) runs into this without ever touching cache or session scoping.

I think we could fix this by scoping cache and sessions inside the configured path when suffix_storage_path is false, so storage/framework/cache/data/tenant1 instead of storage/tenant1/framework/cache/data. Tenants stay separated, storage_path() stays central like the user asked for, and there's no storage/tenant<key>/ directory for cleanup to miss.

Note that suffix_storage_path isn't documented anywhere in the v4 docs, it's only documented by the docblock in the config file. The cache/session scoping sections describe the storage/tenant{id}/framework/... structure as the behavior, with no mention that anything changes it.

  • Delete the notes about regression in the tests after reviewing the PR fully (EDIT: deleted: aabba92)
  • Decide whether we should add the path_override thing

Summary by CodeRabbit

Bug Fixes

  • Improved isolation for file-based caches and sessions across tenant contexts.
  • Preserved central cache and session locations when switching contexts.
  • Kept cache and lock directories independently configured and scoped.
  • Added reliable handling for custom cache locations and optional lock directories.
  • Restored original cache and session settings when reverting tenant context.
  • Prevented unsupported, missing, or dynamically added cache stores from being incorrectly scoped.

Tests

  • Expanded coverage for tenant isolation, custom directories, lock paths, sessions, and restoration scenarios.

The tests cover the current (mostly incorrect) scopeCache() behavior (= hardcoding the /framework/cache/data path regardless of what was configured).

The 'file cache stores are separated per tenant' is not a regression test -- it covers the default path, which already worked correctly, there were just no tests for it. The rest are regression tests (see the "NOTE ABOUT REGRESSION" comments -- these are temporary, added them just so that it's clear what's currently wrong or broken) that should be fixed by the FS bootstrapper fix in the next commit.
scopeCache() rewrote path and lock_path for every file-driver store to a hardcoded '<storage>/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).
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

File cache stores now retain original paths, apply tenant-specific path and lock_path values independently, update active FileStore instances, and restore paths after tenancy ends. Session paths use the same scoping logic. Tests cover cache isolation, custom paths, restoration, dynamic stores, and external paths.

Changes

Filesystem cache and session scoping

Layer / File(s) Summary
Per-store cache and session path scoping
src/Bootstrappers/FilesystemTenancyBootstrapper.php
Captures original cache and lock paths, scopes central and external paths per tenant, updates active FileStore instances, reuses the logic for sessions, and restores captured values.
Cache isolation and restoration coverage
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
Tests tenant isolation, central values, separate cache and lock paths, disabled scoping, dynamic stores, missing stores, and external paths.
Custom session path validation
tests/SessionSeparationTest.php
Tests custom file-session paths, tenant and central file placement, directory creation, restoration, and file counts.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Tenancy
  participant FilesystemTenancyBootstrapper
  participant CacheConfig
  participant FileStore
  Tenancy->>FilesystemTenancyBootstrapper: initialize tenant
  FilesystemTenancyBootstrapper->>CacheConfig: scope cache, lock, and session paths
  FilesystemTenancyBootstrapper->>FileStore: apply scoped cache and lock paths
  Tenancy->>FilesystemTenancyBootstrapper: revert tenant
  FilesystemTenancyBootstrapper->>CacheConfig: restore original paths
Loading

Possibly related PRs

Poem

A rabbit checks each cache lane,
Tenant paths stay separate and plain.
Lock paths follow each store,
Central paths return once more.
Session files keep their place.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: preserving configured cache and session paths in FilesystemTenancyBootstrapper.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch scope-cache-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.75%. Comparing base (e0990a4) to head (ae61e8b).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1473      +/-   ##
============================================
+ Coverage     86.65%   86.75%   +0.10%     
- Complexity     1220     1228       +8     
============================================
  Files           186      186              
  Lines          3589     3601      +12     
============================================
+ Hits           3110     3124      +14     
+ Misses          479      477       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lukinovec

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Bootstrappers/FilesystemTenancyBootstrapper.php`:
- Around line 220-244: Validate the original cache path in the cache-scoping
flow before passing it to scopeCachePath(); when a file-driver store omits path,
fail with a clear configuration error or skip the store consistently during
bootstrap and revert. Preserve the existing optional lock_path handling and
ensure scopeCachePath() is never called with null.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 31b4c918-c01e-4d38-bdde-b69db2e32054

📥 Commits

Reviewing files that changed from the base of the PR and between 553f57a and 483a3ec.

📒 Files selected for processing (2)
  • src/Bootstrappers/FilesystemTenancyBootstrapper.php
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

Comment thread src/Bootstrappers/FilesystemTenancyBootstrapper.php Outdated
scopeCache() didn't feel right since it 1) stored thee original paths, 2) actually scoped things. Separate the concerns so that scopeCache() just does that -- scopes cache.
@lukinovec

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

Making it protected could be a minor bc, and it'd be inconsistent with scopeSessions (which is public).
@lukinovec

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@lukinovec lukinovec changed the title Fix FilesystemTenancyBootstrapper::scopeCache() discarding configured cache paths [MINOR BC] Fix FilesystemTenancyBootstrapper::scopeCache() discarding configured cache paths Aug 3, 2026
@lukinovec lukinovec changed the title [MINOR BC] Fix FilesystemTenancyBootstrapper::scopeCache() discarding configured cache paths [MINOR BC] [4.x] Fix FilesystemTenancyBootstrapper::scopeCache() discarding configured cache paths Aug 3, 2026
@lukinovec
lukinovec marked this pull request as ready for review August 3, 2026 15:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php (1)

507-508: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use a unique temporary directory.

This test recursively deletes the fixed /tmp/tenancy-cache-test directory. Another local process or parallel test run can use that directory. The test can delete unrelated data and can conflict with another run.

Generate the path from sys_get_temp_dir() with a random suffix.

Proposed fix
-    $path = '/tmp/tenancy-cache-test';
+    $path = sys_get_temp_dir() . '/tenancy-cache-test-' . bin2hex(random_bytes(8));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 507 -
508, Update the temporary path setup in the affected test to derive the
directory from sys_get_temp_dir() and append a unique random suffix, then
continue passing that generated path to File::deleteDirectory. Ensure each test
run targets only its own temporary directory instead of the fixed
tenancy-cache-test path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Around line 507-508: Update the temporary path setup in the affected test to
derive the directory from sys_get_temp_dir() and append a unique random suffix,
then continue passing that generated path to File::deleteDirectory. Ensure each
test run targets only its own temporary directory instead of the fixed
tenancy-cache-test path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 02a24bdd-540d-4eeb-b1ee-43c71eb6dcc5

📥 Commits

Reviewing files that changed from the base of the PR and between aabba92 and 6b62798.

📒 Files selected for processing (1)
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php (1)

471-500: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that an absent lock_path remains null.

The current assertions pass if scopeCache() replaces an absent lock_path with path. Assert that cache.stores.foo_file.lock_path is null after initialization and after tenancy()->end().

Proposed test assertions
     tenancy()->initialize(Tenant::create());

+    expect(config('cache.stores.foo_file.lock_path'))->toBeNull();
+
     expect(Cache::store('foo_file')->put('key', 'tenant'))->toBeTrue();
     expect(Cache::store('foo_file')->lock('foo')->get())->toBeTrue();

     tenancy()->end();

+    expect(config('cache.stores.foo_file.lock_path'))->toBeNull();
+
     expect(Cache::store('foo_file')->put('key', 'central'))->toBeTrue();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 471 -
500, Update the test around scopeCache() to assert that
cache.stores.foo_file.lock_path remains null after tenancy()->initialize() and
again after tenancy()->end(). Keep the existing cache put and lock behavior
assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Bootstrappers/FilesystemTenancyBootstrapper.php`:
- Around line 196-213: In src/Bootstrappers/FilesystemTenancyBootstrapper.php at
the bootstrap-time loop (lines 196-213), record the names of file stores that
are successfully scoped into a new instance property (for example, a scoped
stores list). During revert, update the scopeCache(false) method to iterate over
this captured snapshot of scoped stores instead of reading from the current
tenancy.cache.stores configuration list, ensuring that stores removed
mid-request are still reverted. In
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php at lines 578-618, add
a test case that scopes a file store during bootstrap, then removes it from
tenancy.cache.stores before calling tenancy()->end(), and asserts that the
store's path, lock_path, and resolved FileStore instance are restored to their
central-context values on revert.

---

Outside diff comments:
In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Around line 471-500: Update the test around scopeCache() to assert that
cache.stores.foo_file.lock_path remains null after tenancy()->initialize() and
again after tenancy()->end(). Keep the existing cache put and lock behavior
assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f5703c71-3aab-4cb7-850a-0f98033cc0f6

📥 Commits

Reviewing files that changed from the base of the PR and between 6b62798 and 0765bfc.

📒 Files selected for processing (2)
  • src/Bootstrappers/FilesystemTenancyBootstrapper.php
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

Comment thread src/Bootstrappers/FilesystemTenancyBootstrapper.php Outdated
Note: 'the original cache paths are only stored on the first bootstrap' test got removed  -- it tested that the "Unable to create tenant session directory" exception gets thrown, and that's not in scope of the current PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php (3)

525-540: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that lock_path remains null.

The cache and lock operations also pass if the bootstrapper replaces an absent lock_path with the scoped cache path. Assert that cache.stores.foo_file.lock_path is null during tenancy and after tenancy()->end().

Based on upstream contract: scopeCache() preserves an absent lock_path as null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 525 -
540, Extend the test around the foo_file store configuration to assert that
cache.stores.foo_file.lock_path remains null both after tenancy initialization
and after tenancy()->end(). Keep the existing cache and lock operation
assertions unchanged, verifying scopeCache() preserves the absent lock_path
rather than replacing it with the scoped cache path.

493-505: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Release each acquired file lock.

Both tests call get() on non-expiring locks and do not release them. The cleanup only deletes the central configured directory. It does not remove the scoped tenant lock directory. A reused tenant suffix can then make a later lock acquisition fail.

  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php#L493-L505: retain the tenant and central lock instances, then call release() before cleanup.
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php#L534-L540: retain the tenant and central fallback lock instances, then call release() before cleanup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 493 -
505, Release every acquired non-expiring file lock before cleanup: in
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php lines 493-505, retain
the tenant and central lock instances and call release() on both; apply the same
change to the tenant and central fallback locks at lines 534-540.

550-551: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use an isolated temporary directory.

Line 550 uses a fixed shared directory. File::deleteDirectory() deletes all its contents before and after the test. Parallel test workers can delete each other’s active cache data. Local runs can also delete unrelated data at this path. Generate a unique child directory under the system temporary directory for this fixture.

Also applies to: 586-586

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 550 -
551, Replace the fixed shared directory path `/tmp/tenancy-cache-test` with a
dynamically generated unique temporary directory. Generate a unique child
directory under the system temporary directory for the $path variable
assignment, then pass this unique path to File::deleteDirectory(). Apply the
same fix to both occurrences at lines 550 and 586 to prevent parallel test
workers and local runs from deleting each other's or unrelated cache data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Around line 525-540: Extend the test around the foo_file store configuration
to assert that cache.stores.foo_file.lock_path remains null both after tenancy
initialization and after tenancy()->end(). Keep the existing cache and lock
operation assertions unchanged, verifying scopeCache() preserves the absent
lock_path rather than replacing it with the scoped cache path.
- Around line 493-505: Release every acquired non-expiring file lock before
cleanup: in tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php lines
493-505, retain the tenant and central lock instances and call release() on
both; apply the same change to the tenant and central fallback locks at lines
534-540.
- Around line 550-551: Replace the fixed shared directory path
`/tmp/tenancy-cache-test` with a dynamically generated unique temporary
directory. Generate a unique child directory under the system temporary
directory for the $path variable assignment, then pass this unique path to
File::deleteDirectory(). Apply the same fix to both occurrences at lines 550 and
586 to prevent parallel test workers and local runs from deleting each other's
or unrelated cache data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2106cfba-9413-4eb9-8649-04256ab95fb9

📥 Commits

Reviewing files that changed from the base of the PR and between 0765bfc and 597e48e.

📒 Files selected for processing (1)
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

Also add a separate test ('scopeCache ignores changes to tenancy.cache.stores made in tenant context' ) -- the 'central cache is not lost when tenancy ends' covered the skipping mechanism partially, but having a separate test for the tenancy.cache.stores mid-tenant context changes is definitely cleaner and makes more sense.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Bootstrappers/FilesystemTenancyBootstrapper.php (1)

17-18: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear the cache-path snapshot after each revert.

$originalCachePaths and $originalCacheLockPaths remain populated after revert(). If central code changes a scoped store path or lock_path after tenancy ends, Line 211 skips the new values. The next bootstrap scopes and restores the stale first values.

Clear both maps after the revert loop. Add a regression test that changes both paths after tenancy()->end(), then initializes another tenant and verifies the new paths are scoped and restored.

Proposed fix
             $store->setDirectory($path);
             $store->setLockDirectory($lockPath);
         }
+
+        if ($suffix === false) {
+            $this->originalCachePaths = [];
+            $this->originalCacheLockPaths = [];
+        }
     }

Also applies to: 211-214

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bootstrappers/FilesystemTenancyBootstrapper.php` around lines 17 - 18,
Update FilesystemTenancyBootstrapper::revert() to clear both originalCachePaths
and originalCacheLockPaths after completing the revert loop, so each subsequent
bootstrap snapshots current path values. Add a regression test covering changes
to both path and lock_path after tenancy()->end(), then verify the next tenant
scopes those new paths and restores them afterward.
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php (1)

579-580: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use a unique cross-platform temporary directory.

/tmp/tenancy-cache-test is shared by test workers. The cleanup can delete artifacts from another run or local data with the same path. The fixed /tmp path also prevents this test from running on platforms without /tmp.

Proposed fix
-    $path = '/tmp/tenancy-cache-test';
+    $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'tenancy-cache-' . bin2hex(random_bytes(8));

Also applies to: 615-615

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 579 -
580, Replace the hard-coded /tmp/tenancy-cache-test path in the affected test
setup and cleanup blocks with a unique, cross-platform temporary directory
generated through the project’s existing temporary-directory utility, and reuse
that generated path throughout each test run. Apply the same change to the
additional occurrence noted in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/Bootstrappers/FilesystemTenancyBootstrapper.php`:
- Around line 17-18: Update FilesystemTenancyBootstrapper::revert() to clear
both originalCachePaths and originalCacheLockPaths after completing the revert
loop, so each subsequent bootstrap snapshots current path values. Add a
regression test covering changes to both path and lock_path after
tenancy()->end(), then verify the next tenant scopes those new paths and
restores them afterward.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Around line 579-580: Replace the hard-coded /tmp/tenancy-cache-test path in
the affected test setup and cleanup blocks with a unique, cross-platform
temporary directory generated through the project’s existing temporary-directory
utility, and reuse that generated path throughout each test run. Apply the same
change to the additional occurrence noted in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 514d9e23-ec4f-4f08-9d0e-5dbda57b548d

📥 Commits

Reviewing files that changed from the base of the PR and between 0765bfc and 8244e56.

📒 Files selected for processing (2)
  • src/Bootstrappers/FilesystemTenancyBootstrapper.php
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

The check could only pass with a `null` path, which is just bad configuration. So no reason to keep this.
Add more meaningful assertions, make the tests clearer (by improving the names, commnets and the test code itself), merge separate tests that don't need to be separate. Also, don't set lock_path in tests that don't deal with locks (except for the generic "file cache stores are separated per tenant" test where we just want to mirror Laravel's default file store config -- though note that keeping the lock_path unset or null there would make no difference).
Fails at :108 (= the bootstrap() behavior), the session path ends with framework/sessions instead of the configured framework/foo_session)). After commenting out  :108 and :110, it fails at :115 (= the revert() behavior, instead of reverting to the original configured path -- framework/foo_sessions -- it reverts to the hardcoded framework/sessions path)
…th instead of hardcoding /framework/sessions

Also rename tenantCachePath to tenantScopedPath since it's now used both for scoping cache and session paths.

Note that the $originalPath local variable -- added that to avoid PHPStan errors.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php (1)

323-331: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that initialization throws no exception.

not()->toThrow(ErrorException::class) passes if initialization throws a different exception type. Use not()->toThrow() because this test claims that non-file and missing stores are skipped without failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 323 -
331, Update the assertion in the tenancy initialization test around
tenancy()->initialize($tenant1) to use not()->toThrow() without restricting the
exception class, ensuring initialization fails for no exception type when
non-file and missing stores are skipped.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Bootstrappers/FilesystemTenancyBootstrapper.php`:
- Around line 217-225: Update the revert logic in FilesystemTenancyBootstrapper
so scope_cache controls only bootstrapping; always restore every captured cache
store from originalCachePaths and originalCacheLockPaths regardless of the
current flag. Likewise, make scope_sessions affect only bootstrapping and
restore originalSessionPath whenever it was captured, even if the flag changed;
add regressions covering each flag being disabled after tenant bootstrap and
verifying central paths are restored. For the affected sites, apply this at
src/Bootstrappers/FilesystemTenancyBootstrapper.php lines 217-225 and 263-268.

In `@tests/SessionSeparationTest.php`:
- Line 100: Ensure the configured session directory exists before the
File::cleanDirectory call in the session-separation test, using the framework’s
directory-creation helper. Keep the existing cleanup and subsequent
File::files($configuredSessionPath) verification unchanged.

---

Outside diff comments:
In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Around line 323-331: Update the assertion in the tenancy initialization test
around tenancy()->initialize($tenant1) to use not()->toThrow() without
restricting the exception class, ensuring initialization fails for no exception
type when non-file and missing stores are skipped.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b4110ec7-e435-474d-b5af-61771690d0ff

📥 Commits

Reviewing files that changed from the base of the PR and between 3e564c1 and 8c8bd6e.

📒 Files selected for processing (3)
  • src/Bootstrappers/FilesystemTenancyBootstrapper.php
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
  • tests/SessionSeparationTest.php

Comment thread src/Bootstrappers/FilesystemTenancyBootstrapper.php
Comment thread tests/SessionSeparationTest.php
@lukinovec lukinovec changed the title [MINOR BC] [4.x] Fix FilesystemTenancyBootstrapper::scopeCache() discarding configured cache paths [MINOR BC] [4.x] Fix FilesystemTenancyBootstrapper::scopeCache() discarding configured cache and session paths Aug 6, 2026
@lukinovec lukinovec changed the title [MINOR BC] [4.x] Fix FilesystemTenancyBootstrapper::scopeCache() discarding configured cache and session paths [MINOR BC] [4.x] Fix FilesystemTenancyBootstrapper discarding configured cache and session paths Aug 6, 2026
The assertion could cause false positives/negatives since the exception is not that specific. Just let tenancy()->initialize($tenant1) run and fail loudly, that could tell us more about what's wrong than the original assertion.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php (2)

606-606: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the central cache directory assertion.

storage_path('framework/cache/data') is the central cache directory. The earlier test writes to it at Line 323, so this directory can exist even when the external cache path is scoped correctly. Assert the configured store path instead, or check the tenant-scoped default path.

Proposed fix
-    expect(File::isDirectory(storage_path('framework/cache/data')))->toBeFalse();
+    expect(config('cache.stores.foo_file.path'))
+        ->toBe("{$path}/tenant{$tenant1->id}");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` at line 606,
Update the directory assertion in the relevant filesystem tenancy test to check
the configured external cache store path or tenant-scoped default path, rather
than storage_path('framework/cache/data'). Preserve the expectation that the
scoped cache directory is absent.

469-572: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clean up tenant-scoped lock directories.

The tests acquire locks under tenant-scoped paths at Lines 497-498, but cleanup at Lines 517-518 and Line 571 removes only the central paths. If tenant IDs reset between runs, stale tenant lock files can make the next lock assertion fail. Remove the tenant-scoped directories in finally cleanup as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 469 -
572, Update both lock-directory tests around the tenant-scoped paths
($tenantPath and $tenantLockPath) to guarantee cleanup in finally blocks,
deleting the tenant directories as well as the existing central paths. Preserve
the assertions and ensure cleanup runs even when an assertion or lock
acquisition fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Line 606: Update the directory assertion in the relevant filesystem tenancy
test to check the configured external cache store path or tenant-scoped default
path, rather than storage_path('framework/cache/data'). Preserve the expectation
that the scoped cache directory is absent.
- Around line 469-572: Update both lock-directory tests around the tenant-scoped
paths ($tenantPath and $tenantLockPath) to guarantee cleanup in finally blocks,
deleting the tenant directories as well as the existing central paths. Preserve
the assertions and ensure cleanup runs even when an assertion or lock
acquisition fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c1065352-3b4c-4fe0-ac93-4731cfe796b6

📥 Commits

Reviewing files that changed from the base of the PR and between 8c8bd6e and ae61e8b.

📒 Files selected for processing (1)
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants