Skip to content

[6.x] Give unsaved entries their own blueprint cache key - #15509

Open
lwekuiper wants to merge 2 commits into
statamic:6.xfrom
lwekuiper:fix/6.x-unsaved-entry-blueprint-cache-key
Open

lwekuiper wants to merge 2 commits into
statamic:6.xfrom
lwekuiper:fix/6.x-unsaved-entry-blueprint-cache-key

Conversation

@lwekuiper

@lwekuiper lwekuiper commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Entry::blueprint() memoizes the resolved blueprint in Blink under entry-{$this->id()}-blueprint. An unsaved entry has no id yet, so every unsaved entry in the process shares the key entry--blueprint and reads back whichever blueprint resolved first, regardless of which collection it belongs to.

In a web request this is hard to notice, since you rarely create more than one entry. In a long-lived queue worker it bites. I ran into it with an importer that creates entries in two collections from the same job: almost every entry in one collection was saved with the other collection's blueprint handle, and from then on they threw a BlueprintNotFoundException on hydration, since that handle does not exist in their own collection.

Minimal reproduction, no queue needed:

$blogEntry = (new Entry)->collection($blog);        // neither is saved,
$productEntry = (new Entry)->collection($products); // so neither has an id

$blogEntry->blueprint();    // post
$productEntry->blueprint(); // post, expected product

Whichever entry resolves first wins the key. It is symmetric, so the direction of the corruption just depends on which collection gets touched first.

The fix falls back to the object id when there is no entry id, so each instance gets its own key and the memoization is kept as is.

The obj- prefix keeps object ids out of the entry id namespace. spl_object_id() returns small integers and ids are reused once an object is freed, so without it an unsaved entry could take the key of a saved entry with the id 1. That is reachable on the Eloquent driver, where the default entries table uses auto-incrementing ids. Doctrine keeps the same two keyspaces in separate maps for the same reason.

Same shape as #15330, where runtime form blueprints shared a blank blueprint-contents-- key, and #14739, where Bard and Link shared one key for two different types.

Note on the changed test

it_respects_custom_blueprint_template_path_per_collection needed a small fixture change. It builds an unsaved entry in pages using a blueprint saved into collections.articles, which pages cannot resolve. It only passed because the articles entry resolved first and populated the shared key, so the pages entry read the articles blueprint back out.

With the fix it correctly throws BlueprintNotFoundException, so I gave the pages collection its own copy of the blueprint. The assertions it actually cares about, the template path prefixes, are unchanged and still pass.

This branch has not been deployed

No deployments
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