Skip to content

[6.x] Stop unsaved entries from inheriting the blueprint of a freed entry - #15642

Open
alexmzk wants to merge 1 commit into
statamic:6.xfrom
alexmzk:fix/unsaved-entry-blueprint-key-reuse
Open

alexmzk wants to merge 1 commit into
statamic:6.xfrom
alexmzk:fix/unsaved-entry-blueprint-key-reuse

Conversation

@alexmzk

@alexmzk alexmzk commented Oct 13, 2026

Copy link
Copy Markdown

Follow-up to #15509, which keys the blueprint cache of unsaved entries by spl_object_id().

The problem

PHP recycles object ids as soon as an object is freed. When an unsaved entry resolves its blueprint and is then garbage collected, the next entry that happens to receive the same object id finds that blueprint in Blink under entry-obj-{id}-blueprint and returns it, even though it belongs to a different collection. Entry::blueprint() never reaches Collection::entryBlueprint() for that entry.

With the eloquent driver this is persisted: toModel() resolves the blueprint before the model has an id, so the foreign handle lands in the blueprint column, and loading the entry afterwards throws BlueprintNotFoundException ("Blueprint [] not found" because the handle came from the cache, not from the entry's data).

Reproduction on 6.35.1 (two collections, pages and programs):

for ($i = 0; $i < 300; $i++) {
    $page = Entry::make()->collection('pages');
    $page->blueprint();
    unset($page);

    $handle = Entry::make()->collection('programs')->blueprint()->handle(); // 'page' in 179 of 300 rounds
}

In our test suite this showed up as three order-dependent failures after upgrading from 6.34.1.

The fix

Unsaved entries get an instance id from a counter that is never reused, instead of spl_object_id(). Saved entries keep their id-based key. The new test resolves a blog entry's blueprint, frees the entry, and asserts that a freshly made product entry still resolves its own blueprint, in a loop so that an object id does get recycled. The test fails on 6.x before this change and passes with it.

This PR was prepared with an AI tool; the change and the test were reviewed and run locally (tests/Data/Entries/EntryTest.php, 152 tests green).

…ntry

The blueprint cache key of an unsaved entry is built from spl_object_id(),
but PHP recycles object ids as soon as an object is freed. A fresh unsaved
entry can therefore find the blueprint of a dead entry from another
collection in Blink and keep it; the eloquent driver then persists that
handle and the entry throws BlueprintNotFoundException once it is loaded
again. Hand out instance ids that are never reused instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

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