From 21fcc14d978dafd67bbc5c1df73f833388128076 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Tue, 13 Oct 2026 15:06:51 +0200 Subject: [PATCH 1/2] Keep blueprint fields cached when only the parent changes --- src/Fields/Blueprint.php | 10 ++++++- tests/Data/Entries/EntryTest.php | 34 ++++++++++++++++++++++ tests/Fields/BlueprintTest.php | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/Fields/Blueprint.php b/src/Fields/Blueprint.php index 4591b368ef2..b71d5755f37 100644 --- a/src/Fields/Blueprint.php +++ b/src/Fields/Blueprint.php @@ -334,7 +334,15 @@ public function setParent($parent) { $this->parent = $parent; - $this->resetBlueprintCache()->resetFieldsCache(); + $this->resetFieldsCache(); + + // The cache survives when the new parent uses the same blueprint. Give the new + // parent copies of the fields, so fields handed out earlier keep their parent. + if ($this->fieldsCache) { + $this->fieldsCache = $this->fieldsCache->newInstance() + ->setFields($this->fieldsCache->all()->map(fn ($field) => clone $field)) + ->setParent($parent); + } return $this; } diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php index 1b37cb24581..27e2257eb57 100644 --- a/tests/Data/Entries/EntryTest.php +++ b/tests/Data/Entries/EntryTest.php @@ -4,6 +4,7 @@ use BadMethodCallException; use Facades\Statamic\Fields\BlueprintRepository; +use Facades\Statamic\Fields\FieldRepository; use Facades\Statamic\Stache\Repositories\CollectionTreeRepository; use Facades\Tests\Factories\EntryFactory; use Illuminate\Contracts\Support\Arrayable; @@ -32,6 +33,7 @@ use Statamic\Facades; use Statamic\Facades\Blink; use Statamic\Fields\Blueprint; +use Statamic\Fields\Field; use Statamic\Fields\Fieldtype; use Statamic\Fields\Value; use Statamic\Sites\Site; @@ -1273,6 +1275,38 @@ public function it_gets_and_sets_the_published_state() $this->assertFalse($entry->published()); } + #[Test] + public function entries_with_the_same_blueprint_resolve_its_fields_once() + { + $finds = 0; + FieldRepository::shouldReceive('find') + ->with('fieldset_one.field_one') + ->andReturnUsing(function () use (&$finds) { + $finds++; + + return new Field('field_one', ['type' => 'text']); + }); + + BlueprintRepository::shouldReceive('in')->with('collections/blog')->andReturn(collect([ + 'article' => (new Blueprint)->setHandle('article')->setContents([ + 'fields' => [['handle' => 'one', 'field' => 'fieldset_one.field_one']], + ]), + ])); + Collection::make('blog')->dated(true)->save(); + + // Setting the date reads the blueprint's date field, as it happens for every entry loaded from storage. + $makeEntry = fn ($i) => (new Entry)->id("entry-{$i}")->collection('blog')->blueprint('article')->date("2024-01-0{$i}"); + + $this->assertSame($entry = $makeEntry(1), $entry->blueprint()->field('one')->parent()); + $findsForOneBuild = $finds; + + foreach (range(2, 5) as $i) { + $this->assertSame($entry = $makeEntry($i), $entry->blueprint()->field('one')->parent()); + } + + $this->assertSame($findsForOneBuild, $finds); + } + #[Test] public function it_gets_the_blueprint_when_defined_on_itself() { diff --git a/tests/Fields/BlueprintTest.php b/tests/Fields/BlueprintTest.php index 5a947455ed8..c3da8632f72 100644 --- a/tests/Fields/BlueprintTest.php +++ b/tests/Fields/BlueprintTest.php @@ -868,6 +868,56 @@ public function it_can_add_fields_multiple_times() } + #[Test] + public function it_keeps_the_fields_when_the_parent_changes_to_one_with_the_same_blueprint() + { + $finds = 0; + FieldRepository::shouldReceive('find') + ->with('fieldset_one.field_one') + ->andReturnUsing(function () use (&$finds) { + $finds++; + + return new Field('field_one', ['type' => 'text']); + }); + + $blueprint = (new Blueprint) + ->setNamespace('collections/collection_one') + ->setHandle('blueprint_one') + ->setContents(['fields' => [['handle' => 'one', 'field' => 'fieldset_one.field_one']]]); + + $collection = tap(StatamicCollection::make('collection_one'))->save(); + $entryA = (new Entry)->collection($collection)->blueprint('blueprint_one'); + $entryB = (new Entry)->collection($collection)->blueprint('blueprint_one'); + + $this->assertSame($entryA, $blueprint->setParent($entryA)->field('one')->parent()); + $findsForOneBuild = $finds; + + foreach ([$entryB, $entryA, $entryB] as $entry) { + $this->assertSame($entry, $blueprint->setParent($entry)->field('one')->parent()); + } + + $this->assertSame($findsForOneBuild, $finds); + } + + #[Test] + public function fields_keep_their_parent_when_the_blueprint_moves_to_another_parent() + { + $blueprint = (new Blueprint) + ->setNamespace('collections/collection_one') + ->setHandle('blueprint_one') + ->setContents(['fields' => [['handle' => 'one', 'field' => ['type' => 'text']]]]); + + $collection = tap(StatamicCollection::make('collection_one'))->save(); + $entryA = (new Entry)->collection($collection)->blueprint('blueprint_one'); + $entryB = (new Entry)->collection($collection)->blueprint('blueprint_one'); + + $fieldA = $blueprint->setParent($entryA)->field('one'); + $fieldB = $blueprint->setParent($entryB)->field('one'); + + $this->assertSame($entryA, $fieldA->parent()); + $this->assertSame($entryB, $fieldB->parent()); + } + #[Test] public function it_ensures_a_field_has_config() { From 9261e7c8ad3ef87b9ffc75355b5fd99f0e4600ff Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 15 Oct 2026 19:49:22 -0400 Subject: [PATCH 2/2] Test blueprint field parents after publishing or unpublishing a working copy Guards the behavior from #11116 now that setParent() can keep the cached fields. Co-Authored-By: Claude Sonnet 5.5 --- tests/Feature/Entries/EntryRevisionsTest.php | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Feature/Entries/EntryRevisionsTest.php b/tests/Feature/Entries/EntryRevisionsTest.php index 573fa834042..cddb4d23d56 100644 --- a/tests/Feature/Entries/EntryRevisionsTest.php +++ b/tests/Feature/Entries/EntryRevisionsTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Events\EntryDeleted; use Statamic\Events\EntryDeleting; @@ -326,6 +327,39 @@ public function it_unpublishes_an_entry() $this->assertEquals('unpublish', $revision->action()); } + #[Test] + #[DataProvider('workingCopyActionsProvider')] + public function blueprint_fields_point_to_the_new_entry_after_publishing_or_unpublishing_a_working_copy($method) + { + $this->setTestBlueprint('test', ['foo' => ['type' => 'text']]); + + $entry = EntryFactory::id('1') + ->slug('test') + ->collection('blog') + ->published($method === 'unpublishWorkingCopy') + ->date('2010-12-25') + ->data(['blueprint' => 'test', 'title' => 'Title', 'foo' => 'bar']) + ->create(); + + $entry->makeWorkingCopy()->save(); + + $this->assertSame($entry, $entry->blueprint()->setParent($entry)->field('foo')->parent()); + + $published = $entry->{$method}(); + + $this->assertNotSame($entry, $published); + $this->assertSame($published, $published->blueprint()->field('foo')->parent()); + $this->assertSame($published, $published->blueprint()->parent()); + } + + public static function workingCopyActionsProvider() + { + return [ + 'publish' => ['publishWorkingCopy'], + 'unpublish' => ['unpublishWorkingCopy'], + ]; + } + #[Test] public function it_denies_creating_a_revision_without_permission_to_edit_entry() {