From fe2c56f04febd4f9d7630f5a19b4af363dbb70a6 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 22 Feb 2024 14:24:21 -0600 Subject: [PATCH 1/5] Makes method-backed augmented values lazy/deferrable --- src/Data/AbstractAugmented.php | 33 ++++-- src/Data/AugmentedCollection.php | 16 +++ src/Data/HasAugmentedInstance.php | 5 + src/Data/InvokableValue.php | 103 ++++++++++++++++++ src/Fields/Value.php | 5 + src/Providers/CollectionsServiceProvider.php | 10 ++ src/Tags/Structure.php | 2 +- src/View/Antlers/Engine.php | 4 +- .../Runtime/Sandbox/RuntimeValues.php | 2 +- tests/Data/AugmentedTest.php | 2 +- tests/Data/Entries/AugmentedEntryTest.php | 1 + 11 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 src/Data/InvokableValue.php diff --git a/src/Data/AbstractAugmented.php b/src/Data/AbstractAugmented.php index bda4e372522..42f03e86825 100644 --- a/src/Data/AbstractAugmented.php +++ b/src/Data/AbstractAugmented.php @@ -44,20 +44,23 @@ public function select($keys = null) abstract public function keys(); + public function getAugmentedMethodValue($method) + { + if ($this->methodExistsOnThisClass($method)) { + return $this->$method(); + } + + return $this->data->$method(); + } + public function get($handle): Value { $method = Str::camel($handle); if ($this->methodExistsOnThisClass($method)) { - $value = $this->$method(); - - return $value instanceof Value - ? $value - : new Value($value, $method, null, $this->data); - } - - if (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) { - return $this->wrapValue($this->data->$method(), $handle); + return $this->wrapInvokable($method, true, $this, $handle); + } elseif (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) { + return $this->wrapInvokable($method, false, $this->data, $handle); } return $this->wrapValue($this->getFromData($handle), $handle); @@ -93,6 +96,18 @@ protected function getFromData($handle) return $value; } + protected function wrapInvokable(string $method, bool $proxy, $methodTarget, string $handle) + { + $fields = $this->blueprintFields(); + + return (new InvokableValue( + null, + $handle, + optional($fields->get($handle))->fieldtype(), + $this->data + ))->setInvokableDetails($method, $proxy, $methodTarget); + } + protected function wrapValue($value, $handle) { $fields = $this->blueprintFields(); diff --git a/src/Data/AugmentedCollection.php b/src/Data/AugmentedCollection.php index 460300ead98..08c6aad9b59 100644 --- a/src/Data/AugmentedCollection.php +++ b/src/Data/AugmentedCollection.php @@ -47,6 +47,22 @@ public function withoutEvaluation() return $this; } + public function all() + { + return collect($this->items)->map(function ($item) { + if ($item instanceof InvokableValue) { + return $item->materialize(); + } + + return $item; + })->all(); + } + + public function deferredAll() + { + return parent::all(); + } + public function toArray() { return $this->map(function ($value) { diff --git a/src/Data/HasAugmentedInstance.php b/src/Data/HasAugmentedInstance.php index 41d1fb1f013..ed3871a1da6 100644 --- a/src/Data/HasAugmentedInstance.php +++ b/src/Data/HasAugmentedInstance.php @@ -26,6 +26,11 @@ public function toAugmentedArray($keys = null) return $this->toAugmentedCollection($keys)->all(); } + public function toDeferredAugmentedArray($keys = null) + { + return $this->toAugmentedCollection($keys)->deferredAll(); + } + public function toShallowAugmentedCollection() { return $this->augmented()->select($this->shallowAugmentedArrayKeys())->withShallowNesting(); diff --git a/src/Data/InvokableValue.php b/src/Data/InvokableValue.php new file mode 100644 index 00000000000..39828aeb456 --- /dev/null +++ b/src/Data/InvokableValue.php @@ -0,0 +1,103 @@ +proxyThroughAugmented = $proxyCall; + $this->methodName = $method; + $this->methodTarget = $target; + + return $this; + } + + protected function resolve() + { + if ($this->hasResolved) { + return; + } + + if ($this->methodTarget == null) { + $this->hasResolved = true; + + return; + } + + $curIsolationState = GlobalRuntimeState::$requiresRuntimeIsolation; + + GlobalRuntimeState::$requiresRuntimeIsolation = true; + if ($this->proxyThroughAugmented && method_exists($this->methodTarget, 'getAugmentedMethodValue')) { + $this->raw = $this->methodTarget->getAugmentedMethodValue($this->methodName); + + if (! $this->raw instanceof Value) { + // Replicate previous behavior of not having + // a field set if the method call did not + // return a Value instance. + $this->fieldtype = null; + } else { + // Store the original Value instance, if we have it. + $this->resolvedValueInstance = $this->raw; + + // Shift some values around. + $this->fieldtype = $this->raw->fieldtype(); + $this->raw = $this->raw->raw(); + } + } elseif (! $this->proxyThroughAugmented) { + $this->raw = $this->methodTarget->{$this->methodName}(); + } + + $this->methodTarget = null; + + $this->hasResolved = true; + + GlobalRuntimeState::$requiresRuntimeIsolation = $curIsolationState; + } + + public function materialize() + { + $this->resolve(); + + if ($this->resolvedValueInstance != null) { + return $this->resolvedValueInstance; + } + + return $this->toValue(); + } + + protected function toValue() + { + return new Value($this->raw, $this->handle, $this->fieldtype, $this->augmentable, $this->shallow); + } + + public function raw() + { + $this->resolve(); + + return parent::raw(); + } + + public function value() + { + $this->resolve(); + + return parent::value(); + } + + public function shallow() + { + $this->resolve(); + + return parent::shallow(); + } +} diff --git a/src/Fields/Value.php b/src/Fields/Value.php index 2674930a9da..79010ff171f 100644 --- a/src/Fields/Value.php +++ b/src/Fields/Value.php @@ -37,6 +37,11 @@ public function raw() return $this->raw; } + public function materialize() + { + return $this; + } + public function value() { if (! $this->fieldtype) { diff --git a/src/Providers/CollectionsServiceProvider.php b/src/Providers/CollectionsServiceProvider.php index fb1115fbffc..14cc476b5c8 100644 --- a/src/Providers/CollectionsServiceProvider.php +++ b/src/Providers/CollectionsServiceProvider.php @@ -163,6 +163,16 @@ protected function toAugmentedArray() }, $this->items); }); + Collection::macro('toDeferredAugmentedArray', function ($keys = null) { + return array_map(function ($value) use ($keys) { + if ($value instanceof Augmentable) { + return $value->toDeferredAugmentedArray($keys); + } + + return $value instanceof Arrayable ? $value->toArray() : $value; + }, $this->items); + }); + Collection::macro('toAugmentedCollection', function ($keys = null) { return array_map(function ($value) use ($keys) { if ($value instanceof Augmentable) { diff --git a/src/Tags/Structure.php b/src/Tags/Structure.php index bdaecf27080..548f35c1c88 100644 --- a/src/Tags/Structure.php +++ b/src/Tags/Structure.php @@ -123,7 +123,7 @@ public function toArray($tree, $parent = null, $depth = 1) $pages = collect($tree)->map(function ($item, $index) use ($parent, $depth, $tree) { $page = $item['page']; $keys = $this->getQuerySelectKeys($page); - $data = $page->toAugmentedArray($keys); + $data = $page->toDeferredAugmentedArray($keys); $children = empty($item['children']) ? [] : $this->toArray($item['children'], $data, $depth + 1); $url = $page->urlWithoutRedirect(); diff --git a/src/View/Antlers/Engine.php b/src/View/Antlers/Engine.php index d53f9191e61..354891f426c 100644 --- a/src/View/Antlers/Engine.php +++ b/src/View/Antlers/Engine.php @@ -161,11 +161,11 @@ public static function renderTag(Parser $parser, $name, $parameters = [], $conte } if ($output instanceof Collection) { - $output = $output->toAugmentedArray(); + $output = $output->toDeferredAugmentedArray(); } if ($output instanceof Augmentable) { - $output = $output->toAugmentedArray(); + $output = $output->toDeferredAugmentedArray(); } // Allow tags to return an array. We'll parse it for them. diff --git a/src/View/Antlers/Language/Runtime/Sandbox/RuntimeValues.php b/src/View/Antlers/Language/Runtime/Sandbox/RuntimeValues.php index a678ecf3af8..6d8b348c313 100644 --- a/src/View/Antlers/Language/Runtime/Sandbox/RuntimeValues.php +++ b/src/View/Antlers/Language/Runtime/Sandbox/RuntimeValues.php @@ -12,7 +12,7 @@ public static function resolveWithRuntimeIsolation($augmentable) { GlobalRuntimeState::$requiresRuntimeIsolation = true; try { - $value = $augmentable->toAugmentedArray(); + $value = $augmentable->toDeferredAugmentedArray(); } catch (Exception $e) { throw $e; } finally { diff --git a/tests/Data/AugmentedTest.php b/tests/Data/AugmentedTest.php index fc5a709b193..4e2af9c740f 100644 --- a/tests/Data/AugmentedTest.php +++ b/tests/Data/AugmentedTest.php @@ -141,7 +141,7 @@ public function foo() } }; - $this->assertSame($valueInstance, $augmented->get('foo')); + $this->assertSame($valueInstance, $augmented->get('foo')->materialize()); } /** @test */ diff --git a/tests/Data/Entries/AugmentedEntryTest.php b/tests/Data/Entries/AugmentedEntryTest.php index 30878bb37b6..efc9fc7d4b7 100644 --- a/tests/Data/Entries/AugmentedEntryTest.php +++ b/tests/Data/Entries/AugmentedEntryTest.php @@ -23,6 +23,7 @@ class AugmentedEntryTest extends AugmentedTestCase public function it_has_a_parent_method() { $entry = Mockery::mock(Entry::class); + $entry->shouldReceive('blueprint')->zeroOrMoreTimes(); $entry->shouldReceive('parent')->andReturn('the parent'); $augmented = new AugmentedEntry($entry); From 1df8569dcadff664f7ada1d036c9a6bbc4241437 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 22 Feb 2024 17:48:46 -0600 Subject: [PATCH 2/5] A tiny, but not nothing difference --- src/View/Cascade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/Cascade.php b/src/View/Cascade.php index 4c8d2c32b9e..87d97cbecb5 100644 --- a/src/View/Cascade.php +++ b/src/View/Cascade.php @@ -163,7 +163,7 @@ protected function hydrateContent() } $variables = $this->content instanceof Augmentable - ? $this->content->toAugmentedArray() + ? $this->content->toDeferredAugmentedArray() : $this->content->toArray(); foreach ($variables as $key => $value) { From d220d16d5003faaa86bf0a0f47f5d4cdf7622887 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 22 Feb 2024 18:53:20 -0600 Subject: [PATCH 3/5] =?UTF-8?q?=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Data/AbstractAugmented.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Data/AbstractAugmented.php b/src/Data/AbstractAugmented.php index 42f03e86825..11f1cfa34bc 100644 --- a/src/Data/AbstractAugmented.php +++ b/src/Data/AbstractAugmented.php @@ -59,7 +59,9 @@ public function get($handle): Value if ($this->methodExistsOnThisClass($method)) { return $this->wrapInvokable($method, true, $this, $handle); - } elseif (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) { + } + + if (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) { return $this->wrapInvokable($method, false, $this->data, $handle); } From 9cf6cd131ac6adf2e0a7ee6983879092c74578d3 Mon Sep 17 00:00:00 2001 From: John Koster Date: Mon, 26 Feb 2024 17:40:36 -0600 Subject: [PATCH 4/5] Reduce calls to blueprintFields --- src/Auth/AugmentedUser.php | 4 ++-- src/Data/AbstractAugmented.php | 42 +++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/Auth/AugmentedUser.php b/src/Auth/AugmentedUser.php index 296459d4c31..b3a80dc187b 100644 --- a/src/Auth/AugmentedUser.php +++ b/src/Auth/AugmentedUser.php @@ -39,7 +39,7 @@ private function commonKeys() ]; } - public function get($handle): Value + public function get($handle, $fieldtype = null): Value { if ($handle === 'is_user') { return new Value(true, 'is_user', null, $this->data); @@ -57,7 +57,7 @@ public function get($handle): Value return new Value(in_array(Str::after($handle, 'in_'), $this->groups()), $handle, null, $this->data); } - return parent::get($handle); + return parent::get($handle, $fieldtype); } protected function roles() diff --git a/src/Data/AbstractAugmented.php b/src/Data/AbstractAugmented.php index 11f1cfa34bc..33c18e66bb8 100644 --- a/src/Data/AbstractAugmented.php +++ b/src/Data/AbstractAugmented.php @@ -13,6 +13,7 @@ abstract class AbstractAugmented implements Augmented protected $data; protected $blueprintFields; protected $relations = []; + protected $isSelecting = false; public function __construct($data) { @@ -34,11 +35,16 @@ public function select($keys = null) $arr = []; $keys = $this->filterKeys(Arr::wrap($keys ?: $this->keys())); + $fields = $this->blueprintFields(); + + $this->isSelecting = true; foreach ($keys as $key) { - $arr[$key] = $this->get($key); + $arr[$key] = $this->get($key, optional($fields->get($key))->fieldtype()); } + $this->isSelecting = false; + return (new AugmentedCollection($arr))->withRelations($this->relations); } @@ -53,19 +59,28 @@ public function getAugmentedMethodValue($method) return $this->data->$method(); } - public function get($handle): Value + protected function adjustFieldtype($handle, $fieldtype) + { + if ($this->isSelecting || $fieldtype !== null) { + return $fieldtype; + } + + return $this->getFieldtype($handle); + } + + public function get($handle, $fieldtype = null): Value { $method = Str::camel($handle); if ($this->methodExistsOnThisClass($method)) { - return $this->wrapInvokable($method, true, $this, $handle); + return $this->wrapInvokable($method, true, $this, $handle, $fieldtype); } if (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) { - return $this->wrapInvokable($method, false, $this->data, $handle); + return $this->wrapInvokable($method, false, $this->data, $handle, $fieldtype); } - return $this->wrapValue($this->getFromData($handle), $handle); + return $this->wrapValue($this->getFromData($handle), $handle, $fieldtype); } protected function filterKeys($keys) @@ -98,30 +113,35 @@ protected function getFromData($handle) return $value; } - protected function wrapInvokable(string $method, bool $proxy, $methodTarget, string $handle) + protected function wrapInvokable(string $method, bool $proxy, $methodTarget, string $handle, $fieldtype = null) { - $fields = $this->blueprintFields(); + $fieldtype = $this->adjustFieldtype($handle, $fieldtype); return (new InvokableValue( null, $handle, - optional($fields->get($handle))->fieldtype(), + $fieldtype, $this->data ))->setInvokableDetails($method, $proxy, $methodTarget); } - protected function wrapValue($value, $handle) + protected function wrapValue($value, $handle, $fieldtype = null) { - $fields = $this->blueprintFields(); + $fieldtype = $this->adjustFieldtype($handle, $fieldtype); return new Value( $value, $handle, - optional($fields->get($handle))->fieldtype(), + $fieldtype, $this->data ); } + protected function getFieldtype($handle) + { + return optional($this->blueprintFields()->get($handle))->fieldtype(); + } + protected function blueprintFields() { if (! isset($this->blueprintFields)) { From 20c376a9e83de4766941a6ae7bbde00dca016c40 Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 27 Feb 2024 20:06:29 -0600 Subject: [PATCH 5/5] Use deferred augmentation in more places internally Positive improvements across a variety of different situations, particularly the `group_by` modifier --- src/Modifiers/CoreModifiers.php | 6 +++--- src/View/Cascade.php | 2 +- tests/Antlers/Runtime/CoreModifiersTest.php | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index 5858cae1d10..10798eebf59 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -767,7 +767,7 @@ public function get($value, $params) // Convert the item to an array, since we'll want access to all the // available data. Then grab the requested variable from there. - $array = $item instanceof Augmentable ? $item->toAugmentedArray() : $item->toArray(); + $array = $item instanceof Augmentable ? $item->toDeferredAugmentedArray() : $item->toArray(); if ($arrayValue = Arr::get($array, $var)) { return $arrayValue; @@ -845,7 +845,7 @@ private function getGroupByValueFromObject($item, $groupBy) { // Make the array just from the params, so it only augments the values that might be needed. $keys = explode(':', $groupBy); - $context = $item->toAugmentedArray($keys); + $context = $item->toDeferredAugmentedArray($keys); return Antlers::parser()->getVariable($groupBy, $context); } @@ -2047,7 +2047,7 @@ public function scope($value, $params) } if ($value instanceof Collection) { - $value = $value->toAugmentedArray(); + $value = $value->toDeferredAugmentedArray(); } return Arr::addScope($value, $scope); diff --git a/src/View/Cascade.php b/src/View/Cascade.php index 87d97cbecb5..b5159286d97 100644 --- a/src/View/Cascade.php +++ b/src/View/Cascade.php @@ -148,7 +148,7 @@ protected function hydrateGlobals() } if ($mainGlobal = $this->get('global')) { - foreach ($mainGlobal->toAugmentedCollection() as $key => $value) { + foreach ($mainGlobal->toDeferredAugmentedArray() as $key => $value) { $this->set($key, $value); } } diff --git a/tests/Antlers/Runtime/CoreModifiersTest.php b/tests/Antlers/Runtime/CoreModifiersTest.php index 4b7dc8040be..4b71dc98652 100644 --- a/tests/Antlers/Runtime/CoreModifiersTest.php +++ b/tests/Antlers/Runtime/CoreModifiersTest.php @@ -581,6 +581,11 @@ public function toAugmentedArray() ]; } + public function toDeferredAugmentedArray() + { + return $this->toAugmentedArray(); + } + public function toArray() { return $this->toAugmentedArray();