Skip to content

fix(find): key the filter and request caches by the controller's own class - #315

Merged
roncodes merged 2 commits into
release/v1.6.71from
fix/find-filter-cache-per-model-filter
Oct 15, 2026
Merged

roncodes merged 2 commits into
release/v1.6.71from
fix/find-filter-cache-per-model-filter

Conversation

@roncodes

Copy link
Copy Markdown
Member

Problem

On production, a brand-new Storefront network with no stores listed every order in the company on its Orders page. The page asks for GET storefront/int/v1/orders?network=network_rw2htfer7j, and Storefront's order filter limits that to the network's orders. The HAR of that request showed:

  • 246 orders came back, about half without meta.storefront_id. Storefront's filter excludes every such order (queryForInternal requires meta->storefront_id), so Storefront's filter can't have run.
  • The rows were serialized by Storefront's index resource, so it was Storefront's controller that answered.
  • x-cache-status: HIT, so the API list cache kept serving the wrong result for its TTL.

Cause

FleetbaseController::setApiModel() gives the model the controller's filter ($model->filter = $this->filter), and Model::getFilter() returns it. But Find::httpFilterForModel() keeps its answer in a function-level static $cache keyed only by model class, namespace and version, and reads getFilter() only on a miss.

Fleet-Ops and Storefront both list Fleetbase\FleetOps\Models\Order, with different filters. In an Octane worker, whichever order list it serves first decides the filter for every later one:

  • Fleet-Ops first: Storefront's lists get Fleet-Ops' filter, so the network, storefront and storefront_id scopes never apply. This is the reported bug.
  • Storefront first: Fleet-Ops' order lists get Storefront's scopes instead.

Reproduced in tinker, one process:

first (Fleet-Ops controller, no filter set): \Fleetbase\FleetOps\Http\Filter\OrderFilter
then Storefront controller (filter set to Storefront): \Fleetbase\FleetOps\Http\Filter\OrderFilter

The convention fallback (Http\Filter\Internal\v1\Model versus v1\Model) depends on whether the request is internal, and that wasn't part of the key either.

httpResourceForModel() already keys on the model's explicit resource. httpRequestForModel() had the same gap for $model->request.

Fix

  • httpFilterForModel(): the cache key includes the model's own filter (getFilter()). When the filter comes from the convention fallback, both the internal and public answers are cached, and the request picks between them on read, the way httpResourceForModel() does.
  • httpRequestForModel(): the cache key includes the model's own request class (getRequest()). Its fallback still reads the audience only on a miss, as before. Request classes are resolved in controller constructors, and reading the audience there on every call recursed through UserControllerTest's route stub, which builds a controller on each route() call.
  • New Find::versionedClass(), shared by both. It returns the first existing class among Internal\vN\Model (internal routes only), vN\Model and Model, matching the previous str_replace fallbacks.

Tests

Three new cases in tests/Unit/FindHttpRequestForModelTest.php:

  • A filter set on one instance is used even after another instance of the same model resolved the convention filter.
  • A convention filter is resolved per request audience, not by the first audience asked.
  • A request class set on the model is used even after the model resolved the default.

All three fail on release/v1.6.71 and pass with the fix.

Validation, run locally on PHP 8.4:

  • pest (full suite): 2245 tests, 0 failed.
  • php-cs-fixer on the changed files: clean.
  • php -l on PHP 8.2: clean.

Release

  • Added a "Fixed" entry to RELEASE.md for v1.6.71.
  • No migrations. Stale list-cache entries expire within their 5-minute TTL.
  • Workers pick up the fix on deploy (octane:reload).

…class

Find::httpFilterForModel() cached its answer per model class for the life of
the process, ignoring the filter a controller sets on its model and whether
the request was internal. Under Octane, the first controller to list a model
decided the filter for every other: Fleet-Ops' order filter was applied to
Storefront's order lists, so a network's orders page listed every order in the
company. The cache is now keyed by the model's own filter, and a filter found
by convention is picked per request audience. httpRequestForModel() is keyed
by the model's own request class too.
@roncodes
roncodes merged commit 719b438 into release/v1.6.71 Oct 15, 2026
3 checks passed
@roncodes
roncodes deleted the fix/find-filter-cache-per-model-filter branch October 15, 2026 08:42
@codecov

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f4693d4) to head (aa47cc7).
⚠️ Report is 7 commits behind head on release/v1.6.71.

Additional details and impacted files
@@                 Coverage Diff                 @@
##             release/v1.6.71      #315   +/-   ##
===================================================
  Coverage             100.00%   100.00%           
  Complexity              8885      8885           
===================================================
  Files                    472       472           
  Lines                  27934     27925    -9     
===================================================
- Hits                   27934     27925    -9     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.

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.

1 participant