diff --git a/src/Data/ContainsComputedData.php b/src/Data/ContainsComputedData.php index fbe4f321bd2..cf72d60bd2e 100644 --- a/src/Data/ContainsComputedData.php +++ b/src/Data/ContainsComputedData.php @@ -2,6 +2,8 @@ namespace Statamic\Data; +use Statamic\Fields\Value; + trait ContainsComputedData { protected $withComputedData = true; @@ -16,14 +18,21 @@ public function computedKeys() } public function computedData() + { + return $this->getComputedData(false); + } + + public function getComputedData($wrapInValue) { if (! method_exists($this, 'getComputedCallbacks')) { return collect(); } - return collect($this->getComputedCallbacks())->map(function ($callback, $field) { - return $this->getComputed($field); - }); + return collect($this->getComputedCallbacks()) + ->map(fn ($callback, $field) => $wrapInValue ? + new Value(fn () => $this->getComputed($field)) : + $this->getComputed($field) + ); } public function getComputed($key) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index fe045594ee8..5d390cc4b16 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -32,12 +32,17 @@ public function keys() } public function values() + { + return $this->getValues(false); + } + + public function getValues($wrapComputed) { $originFallbackValues = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues() : collect(); $originValues = $this->hasOrigin() ? $this->origin()->values() : collect(); - $computedData = method_exists($this, 'computedData') ? $this->computedData() : []; + $computedData = method_exists($this, 'getComputedData') ? $this->getComputedData($wrapComputed) : []; return collect() ->merge($originFallbackValues) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index e83ed2d3b6f..b3d4cd67846 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -900,7 +900,12 @@ public function route() public function routeData() { - $data = $this->values()->merge([ + // This uses the `getValues(true)` method instead of values() + // This is so we can wrap computed fields in Value so we + // can delay their execution. If the computed value + // triggers the routeData() method, we will end + // up in an infinite loop that is not fun. + $data = $this->getValues(true)->merge([ 'id' => $this->id(), 'slug' => $this->slug(), 'published' => $this->published(), diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php index d5f788782db..19c3846cef4 100644 --- a/tests/Data/Entries/EntryTest.php +++ b/tests/Data/Entries/EntryTest.php @@ -2615,4 +2615,28 @@ public function it_clones_internal_collections() $this->assertEquals('A', $entry->getSupplement('bar')); $this->assertEquals('B', $clone->getSupplement('bar')); } + + #[Test] + public function using_route_data_in_computed_props_does_not_cause_infinite_loops() + { + $collection = + \Statamic\Facades\Collection::make('pages') + ->routes('{slug}') + ->structureContents(['root' => true]) // We need to be in a structure to create the infinite loop condition. + ->save(); + + \Statamic\Facades\Collection::computed('pages', 'custom', function ($entry) { + return 'Custom: '.$entry->uri(); + }); + + EntryFactory::id('entry-id') + ->slug('entry-slug') + ->collection('pages') + ->create(); + + Blink::store('entry-uris')->flush(); + + $entry = Facades\Entry::find('entry-id'); + $this->assertSame('Custom: /', $entry->custom); + } }