Skip to content

Commit ccac3c3

Browse files
jasonvargaclaude
andauthored
[6.x] Give reference updater fields the item being updated as their parent (#15443)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ff25e6a commit ccac3c3

2 files changed

Lines changed: 138 additions & 2 deletions

File tree

src/Data/DataReferenceUpdater.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,23 @@ public function updateReferences($originalValue, $newValue)
8585
*/
8686
protected function getTopLevelFields()
8787
{
88-
return $this->item->blueprint()->fields()->all();
88+
return $this->fieldsWithItemAsParent($this->item->blueprint()->fields());
89+
}
90+
91+
/**
92+
* While updating references, the parent of every field is the item being updated.
93+
*
94+
* Blueprints hand back a Fields instance that's cached globally by handle, and nested fields
95+
* are constructed without a parent at all, so neither can be relied upon to have the right
96+
* one. The fields are cloned rather than mutated in place, otherwise the item would leak
97+
* into every other consumer of that blueprint for the rest of the request.
98+
*
99+
* @param \Statamic\Fields\Fields $fields
100+
* @return \Illuminate\Support\Collection
101+
*/
102+
private function fieldsWithItemAsParent($fields)
103+
{
104+
return $fields->all()->map(fn ($field) => (clone $field)->setParent($this->item));
89105
}
90106

91107
/**
@@ -120,7 +136,7 @@ protected function fieldsWithReferenceUpdates($fields)
120136
*/
121137
public function processNestedFields($fields, $dottedPrefix): void
122138
{
123-
$this->recursivelyUpdateFields($fields->all(), $dottedPrefix);
139+
$this->recursivelyUpdateFields($this->fieldsWithItemAsParent($fields), $dottedPrefix);
124140
}
125141

126142
/**
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
namespace Tests\Data;
4+
5+
use PHPUnit\Framework\Attributes\Test;
6+
use Statamic\Assets\AssetReferenceUpdater;
7+
use Statamic\Facades;
8+
use Statamic\Fields\Fieldtype;
9+
use Statamic\Fieldtypes\UpdatesReferences;
10+
use Tests\PreventSavingStacheItemsToDisk;
11+
use Tests\TestCase;
12+
13+
class DataReferenceUpdaterTest extends TestCase
14+
{
15+
use PreventSavingStacheItemsToDisk;
16+
17+
public function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
ParentRecorderFieldtype::register();
22+
ParentRecorderFieldtype::$parents = [];
23+
24+
tap(Facades\Collection::make('articles'))->save();
25+
}
26+
27+
#[Test]
28+
public function it_gives_top_level_fields_the_item_being_updated_as_their_parent()
29+
{
30+
$this->setInBlueprints('collections/articles', [
31+
'fields' => [
32+
['handle' => 'hero', 'field' => ['type' => 'parent_recorder']],
33+
],
34+
]);
35+
36+
$one = tap(Facades\Entry::make()->collection('articles')->slug('one')->data(['hero' => 'hoff.jpg']))->save();
37+
$two = tap(Facades\Entry::make()->collection('articles')->slug('two')->data(['hero' => 'hoff.jpg']))->save();
38+
39+
$this->updateReferences($one);
40+
$this->updateReferences($two);
41+
42+
$this->assertCount(2, ParentRecorderFieldtype::$parents);
43+
$this->assertSame($one, ParentRecorderFieldtype::$parents[0]);
44+
$this->assertSame($two, ParentRecorderFieldtype::$parents[1]);
45+
}
46+
47+
#[Test]
48+
public function it_gives_nested_fields_the_item_being_updated_as_their_parent()
49+
{
50+
$this->setInBlueprints('collections/articles', [
51+
'fields' => [
52+
[
53+
'handle' => 'grid',
54+
'field' => [
55+
'type' => 'grid',
56+
'fields' => [
57+
['handle' => 'hero', 'field' => ['type' => 'parent_recorder']],
58+
],
59+
],
60+
],
61+
],
62+
]);
63+
64+
$entry = tap(Facades\Entry::make()->collection('articles')->slug('one')->data([
65+
'grid' => [['hero' => 'hoff.jpg']],
66+
]))->save();
67+
68+
$this->updateReferences($entry);
69+
70+
$this->assertCount(1, ParentRecorderFieldtype::$parents);
71+
$this->assertSame($entry, ParentRecorderFieldtype::$parents[0]);
72+
}
73+
74+
#[Test]
75+
public function it_doesnt_leave_the_item_on_the_blueprints_shared_fields()
76+
{
77+
$this->setInBlueprints('collections/articles', [
78+
'fields' => [
79+
['handle' => 'hero', 'field' => ['type' => 'parent_recorder']],
80+
],
81+
]);
82+
83+
$entry = tap(Facades\Entry::make()->collection('articles')->slug('one')->data(['hero' => 'hoff.jpg']))->save();
84+
85+
$blueprint = Facades\Collection::find('articles')->entryBlueprint();
86+
$blueprint->setParent(null);
87+
88+
$this->updateReferences($entry);
89+
90+
$this->assertNull($blueprint->fields()->get('hero')->parent());
91+
}
92+
93+
private function setInBlueprints($namespace, $blueprintContents)
94+
{
95+
$blueprint = tap(Facades\Blueprint::make('set-in-blueprints')->setContents($blueprintContents))->save();
96+
97+
Facades\Blueprint::shouldReceive('in')->with($namespace)->andReturn(collect([$blueprint]));
98+
}
99+
100+
private function updateReferences($item)
101+
{
102+
AssetReferenceUpdater::item($item)
103+
->filterByContainer('test_container')
104+
->updateReferences('hoff.jpg', 'norris.jpg');
105+
}
106+
}
107+
108+
class ParentRecorderFieldtype extends Fieldtype
109+
{
110+
use UpdatesReferences;
111+
112+
public static $parents = [];
113+
114+
public function replaceAssetReferences($data, ?string $newValue, string $oldValue, string $container)
115+
{
116+
static::$parents[] = $this->field->parent();
117+
118+
return $data;
119+
}
120+
}

0 commit comments

Comments
 (0)