From fe2c56f04febd4f9d7630f5a19b4af363dbb70a6 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 22 Feb 2024 14:24:21 -0600 Subject: [PATCH 01/81] 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 02/81] 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 03/81] =?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 eb971c05124a18776c119e42146a8b5be7bcdffc Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 23 Feb 2024 18:19:39 -0600 Subject: [PATCH 04/81] Cache augmentation keys on the instance --- src/Assets/AugmentedAsset.php | 114 +++++++++++++++-------------- src/Auth/AugmentedUser.php | 20 +++-- src/Data/HasOrigin.php | 22 ++++-- src/Entries/AugmentedEntry.php | 16 ++-- src/Filesystem/AbstractAdapter.php | 1 + src/Globals/AugmentedVariables.php | 8 +- src/Structures/AugmentedPage.php | 24 +++--- src/Taxonomies/AugmentedTerm.php | 16 ++-- 8 files changed, 132 insertions(+), 89 deletions(-) diff --git a/src/Assets/AugmentedAsset.php b/src/Assets/AugmentedAsset.php index fb895df50b3..d54346b0b8c 100644 --- a/src/Assets/AugmentedAsset.php +++ b/src/Assets/AugmentedAsset.php @@ -8,64 +8,70 @@ class AugmentedAsset extends AbstractAugmented { + protected $cachedKeys = null; + public function keys() { - $keys = $this->data->data()->keys() - ->merge($this->data->supplements()->keys()) - ->merge([ - 'id', - 'title', - 'path', - 'filename', - 'basename', - 'extension', - 'is_asset', - 'is_audio', - 'is_previewable', - 'is_image', - 'is_svg', - 'is_video', - 'blueprint', - 'edit_url', - 'container', - 'folder', - 'url', - 'permalink', - 'api_url', - ]); - - if ($this->data->exists()) { - $keys = $keys->merge([ - 'size', - 'size_bytes', - 'size_kilobytes', - 'size_megabytes', - 'size_gigabytes', - 'size_b', - 'size_kb', - 'size_mb', - 'size_gb', - 'last_modified', - 'last_modified_timestamp', - 'last_modified_instance', - 'focus', - 'has_focus', - 'focus_css', - 'height', - 'width', - 'orientation', - 'ratio', - 'mime_type', - 'duration', - 'duration_seconds', - 'duration_minutes', - 'duration_sec', - 'duration_min', - 'playtime', - ]); + if (! $this->cachedKeys) { + $keys = $this->data->data()->keys() + ->merge($this->data->supplements()->keys()) + ->merge([ + 'id', + 'title', + 'path', + 'filename', + 'basename', + 'extension', + 'is_asset', + 'is_audio', + 'is_previewable', + 'is_image', + 'is_svg', + 'is_video', + 'blueprint', + 'edit_url', + 'container', + 'folder', + 'url', + 'permalink', + 'api_url', + ]); + + if ($this->data->exists()) { + $keys = $keys->merge([ + 'size', + 'size_bytes', + 'size_kilobytes', + 'size_megabytes', + 'size_gigabytes', + 'size_b', + 'size_kb', + 'size_mb', + 'size_gb', + 'last_modified', + 'last_modified_timestamp', + 'last_modified_instance', + 'focus', + 'has_focus', + 'focus_css', + 'height', + 'width', + 'orientation', + 'ratio', + 'mime_type', + 'duration', + 'duration_seconds', + 'duration_minutes', + 'duration_sec', + 'duration_min', + 'playtime', + ]); + } + + $this->cachedKeys = $keys->merge($this->blueprintFields()->keys())->unique()->all(); } - return $keys->merge($this->blueprintFields()->keys())->unique()->all(); + return $this->cachedKeys; } protected function isAsset() diff --git a/src/Auth/AugmentedUser.php b/src/Auth/AugmentedUser.php index 296459d4c31..a59e832e133 100644 --- a/src/Auth/AugmentedUser.php +++ b/src/Auth/AugmentedUser.php @@ -11,15 +11,21 @@ class AugmentedUser extends AbstractAugmented { + protected $cachedKeys = null; + public function keys() { - return $this->data->data()->keys() - ->merge(collect($this->data->supplements() ?? [])->keys()) - ->merge($this->commonKeys()) - ->merge($this->roleHandles()) - ->merge($this->groupHandles()) - ->merge($this->blueprintFields()->keys()) - ->unique()->sort()->values()->all(); + if (! $this->cachedKeys) { + $this->cachedKeys = $this->data->data()->keys() + ->merge(collect($this->data->supplements() ?? [])->keys()) + ->merge($this->commonKeys()) + ->merge($this->roleHandles()) + ->merge($this->groupHandles()) + ->merge($this->blueprintFields()->keys()) + ->unique()->sort()->values()->all(); + } + + return $this->cachedKeys; } private function commonKeys() diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 186299cfad9..f54bde94ecd 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -11,19 +11,25 @@ trait HasOrigin */ protected $origin; + protected $cachedKeys = null; + public function keys() { - $originFallbackKeys = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues()->keys() : collect(); + if (! $this->cachedKeys) { + $originFallbackKeys = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues()->keys() : collect(); - $originKeys = $this->hasOrigin() ? $this->origin()->keys() : collect(); + $originKeys = $this->hasOrigin() ? $this->origin()->keys() : collect(); - $computedKeys = method_exists($this, 'computedKeys') ? $this->computedKeys() : []; + $computedKeys = method_exists($this, 'computedKeys') ? $this->computedKeys() : []; - return collect() - ->merge($originFallbackKeys) - ->merge($originKeys) - ->merge($this->data->keys()) - ->merge($computedKeys); + $this->cachedKeys = collect() + ->merge($originFallbackKeys) + ->merge($originKeys) + ->merge($this->data->keys()) + ->merge($computedKeys); + } + + return $this->cachedKeys; } public function values() diff --git a/src/Entries/AugmentedEntry.php b/src/Entries/AugmentedEntry.php index 6388f554e81..9cb7f76d830 100644 --- a/src/Entries/AugmentedEntry.php +++ b/src/Entries/AugmentedEntry.php @@ -8,13 +8,19 @@ class AugmentedEntry extends AbstractAugmented { + protected $keysCache = null; + public function keys() { - return $this->data->keys() - ->merge($this->data->supplements()->keys()) - ->merge($this->commonKeys()) - ->merge($this->blueprintFields()->keys()) - ->unique()->sort()->values()->all(); + if (! $this->keysCache) { + $this->keysCache = $this->data->keys() + ->merge($this->data->supplements()->keys()) + ->merge($this->commonKeys()) + ->merge($this->blueprintFields()->keys()) + ->unique()->sort()->values()->all(); + } + + return $this->keysCache; } private function commonKeys() diff --git a/src/Filesystem/AbstractAdapter.php b/src/Filesystem/AbstractAdapter.php index 1a803fa87c8..1130b953623 100644 --- a/src/Filesystem/AbstractAdapter.php +++ b/src/Filesystem/AbstractAdapter.php @@ -83,6 +83,7 @@ public function mimeType($path) public function lastModified($path) { + ray()->count(); return $this->filesystem->lastModified($this->normalizePath($path)); } diff --git a/src/Globals/AugmentedVariables.php b/src/Globals/AugmentedVariables.php index 9eacf32dd98..63ede4006f5 100644 --- a/src/Globals/AugmentedVariables.php +++ b/src/Globals/AugmentedVariables.php @@ -6,9 +6,15 @@ class AugmentedVariables extends AbstractAugmented { + protected $cachedKeys = null; + public function keys() { - return $this->data->values()->keys()->all(); + if (! $this->cachedKeys) { + $this->cachedKeys = $this->data->values()->keys()->all(); + } + + return $this->cachedKeys; } public function site() diff --git a/src/Structures/AugmentedPage.php b/src/Structures/AugmentedPage.php index a3dd2d635dc..c9973557fc8 100644 --- a/src/Structures/AugmentedPage.php +++ b/src/Structures/AugmentedPage.php @@ -22,20 +22,26 @@ public function __construct($page) } } + protected $cachedKeys = null; + public function keys() { - $keys = collect($this->hasEntry - ? parent::keys() - : ['title', 'url', 'uri', 'permalink', 'id']); + if (! $this->cachedKeys) { + $keys = collect($this->hasEntry + ? parent::keys() + : ['title', 'url', 'uri', 'permalink', 'id']); + + $keys = $keys + ->merge($this->page->data()->keys()) + ->merge($this->page->supplements()->keys()) + ->merge(['entry_id']); - $keys = $keys - ->merge($this->page->data()->keys()) - ->merge($this->page->supplements()->keys()) - ->merge(['entry_id']); + $keys = Statamic::isApiRoute() ? $this->apiKeys($keys) : $keys; - $keys = Statamic::isApiRoute() ? $this->apiKeys($keys) : $keys; + $this->cachedKeys = $keys->unique()->sort()->values()->all(); + } - return $keys->unique()->sort()->values()->all(); + return $this->cachedKeys; } private function apiKeys($keys) diff --git a/src/Taxonomies/AugmentedTerm.php b/src/Taxonomies/AugmentedTerm.php index 26cfe1a4574..4ccf00ce428 100644 --- a/src/Taxonomies/AugmentedTerm.php +++ b/src/Taxonomies/AugmentedTerm.php @@ -8,13 +8,19 @@ class AugmentedTerm extends AbstractAugmented { + protected $cachedKeys = null; + public function keys() { - return $this->data->values()->keys() - ->merge($this->data->supplements()->keys()) - ->merge($this->commonKeys()) - ->merge($this->blueprintFields()->keys()) - ->unique()->sort()->values()->all(); + if (! $this->cachedKeys) { + $this->cachedKeys = $this->data->values()->keys() + ->merge($this->data->supplements()->keys()) + ->merge($this->commonKeys()) + ->merge($this->blueprintFields()->keys()) + ->unique()->sort()->values()->all(); + } + + return $this->cachedKeys; } private function commonKeys() From 8296503f3e847d160f6a0bd1298e99c591a561b9 Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 23 Feb 2024 18:23:12 -0600 Subject: [PATCH 05/81] Update AbstractAdapter.php --- src/Filesystem/AbstractAdapter.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Filesystem/AbstractAdapter.php b/src/Filesystem/AbstractAdapter.php index 1130b953623..1a803fa87c8 100644 --- a/src/Filesystem/AbstractAdapter.php +++ b/src/Filesystem/AbstractAdapter.php @@ -83,7 +83,6 @@ public function mimeType($path) public function lastModified($path) { - ray()->count(); return $this->filesystem->lastModified($this->normalizePath($path)); } From f653763073a183757a1b79881b04b5174bb4a693 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 24 Feb 2024 13:57:01 -0600 Subject: [PATCH 06/81] Update Blueprint.php --- src/Fields/Blueprint.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Fields/Blueprint.php b/src/Fields/Blueprint.php index e1278480f96..bee75d6b135 100644 --- a/src/Fields/Blueprint.php +++ b/src/Fields/Blueprint.php @@ -42,6 +42,8 @@ class Blueprint implements Arrayable, ArrayAccess, Augmentable, QueryableValue protected $ensuredFields = []; protected $afterSaveCallbacks = []; protected $withEvents = true; + protected $lastEntryBlueprint = null; + private ?Columns $columns = null; public function setHandle(string $handle) @@ -305,7 +307,18 @@ public function setParent($parent) { $this->parent = $parent; - $this->resetFieldsCache(); + $handle = (function () { + if (property_exists($this, 'blueprint')) { + return $this->blueprint; + } + + return null; + })->call($parent); + + if ($handle == null || $handle != $this->lastEntryBlueprint) { + $this->resetFieldsCache(); + $this->lastEntryBlueprint = $handle; + } return $this; } From d2f212a8b536fe320c7a30e3bbd7e47ceefc010d Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 24 Feb 2024 14:29:59 -0600 Subject: [PATCH 07/81] Refactors/cleanup --- src/Fields/Blueprint.php | 34 ++++++++++++++---------- src/Support/Traits/InvadesProperties.php | 15 +++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 src/Support/Traits/InvadesProperties.php diff --git a/src/Fields/Blueprint.php b/src/Fields/Blueprint.php index bee75d6b135..e71789f5d92 100644 --- a/src/Fields/Blueprint.php +++ b/src/Fields/Blueprint.php @@ -26,10 +26,11 @@ use Statamic\Facades\Path; use Statamic\Support\Arr; use Statamic\Support\Str; +use Statamic\Support\Traits\InvadesProperties; class Blueprint implements Arrayable, ArrayAccess, Augmentable, QueryableValue { - use ExistsAsFile, HasAugmentedData; + use ExistsAsFile, HasAugmentedData, InvadesProperties; protected $handle; protected $namespace; @@ -42,7 +43,7 @@ class Blueprint implements Arrayable, ArrayAccess, Augmentable, QueryableValue protected $ensuredFields = []; protected $afterSaveCallbacks = []; protected $withEvents = true; - protected $lastEntryBlueprint = null; + protected $lastBlueprintHandle = null; private ?Columns $columns = null; @@ -307,18 +308,7 @@ public function setParent($parent) { $this->parent = $parent; - $handle = (function () { - if (property_exists($this, 'blueprint')) { - return $this->blueprint; - } - - return null; - })->call($parent); - - if ($handle == null || $handle != $this->lastEntryBlueprint) { - $this->resetFieldsCache(); - $this->lastEntryBlueprint = $handle; - } + $this->resetFieldsCache(); return $this; } @@ -636,6 +626,22 @@ public function validateUniqueHandles() protected function resetFieldsCache() { + if ($this->parent) { + $blueprintHandle = $this->invade($this->parent, function () { + if (property_exists($this, 'blueprint')) { + return $this->blueprint; + } + + return null; + }); + + if ($blueprintHandle && $blueprintHandle === $this->lastBlueprintHandle) { + return $this; + } + + $this->lastBlueprintHandle = $blueprintHandle; + } + $this->fieldsCache = null; Blink::forget($this->contentsBlinkKey()); diff --git a/src/Support/Traits/InvadesProperties.php b/src/Support/Traits/InvadesProperties.php new file mode 100644 index 00000000000..f28a0979437 --- /dev/null +++ b/src/Support/Traits/InvadesProperties.php @@ -0,0 +1,15 @@ + $this->{$property})->call($object); + } + + return $property->call($object); + } +} From 9dc9b99e3d55fbadc165a8d084adcdb259df4957 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 24 Feb 2024 14:37:03 -0600 Subject: [PATCH 08/81] Code hardening --- src/Support/Traits/InvadesProperties.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Support/Traits/InvadesProperties.php b/src/Support/Traits/InvadesProperties.php index f28a0979437..8596867fcaf 100644 --- a/src/Support/Traits/InvadesProperties.php +++ b/src/Support/Traits/InvadesProperties.php @@ -6,7 +6,11 @@ trait InvadesProperties { protected function invade($object, $property) { - if (! is_callable($property)) { + if (! $property) { + return null; + } + + if (is_string($property) || ! is_callable($property)) { return (fn () => $this->{$property})->call($object); } From 97dd9622eabd0d293a518f3a2a3e1baad7edba4d Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 24 Feb 2024 14:41:27 -0600 Subject: [PATCH 09/81] Removes reflection calls from FluentGetterSetter --- src/Support/FluentGetterSetter.php | 30 ++++----------------- src/Support/Traits/InvadesProperties.php | 34 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 src/Support/Traits/InvadesProperties.php diff --git a/src/Support/FluentGetterSetter.php b/src/Support/FluentGetterSetter.php index f73082a3fd3..3db29a2a0dc 100644 --- a/src/Support/FluentGetterSetter.php +++ b/src/Support/FluentGetterSetter.php @@ -3,11 +3,12 @@ namespace Statamic\Support; use Closure; -use ReflectionException; -use ReflectionObject; +use Statamic\Support\Traits\InvadesProperties; class FluentGetterSetter { + use InvadesProperties; + protected $object; protected $property; protected $getter; @@ -105,11 +106,7 @@ public function args($args) */ protected function runGetterLogic() { - try { - $value = $this->reflectedProperty()->getValue($this->object); - } catch (ReflectionException $exception) { - $value = $this->object->{$this->property} ?? null; - } + $value = $this->invade($this->object, $this->property); if ($getter = $this->getter) { $value = $getter($value); @@ -129,27 +126,10 @@ protected function runSetterLogic($value) $value = $setter($value); } - try { - $this->reflectedProperty()->setValue($this->object, $value); - } catch (ReflectionException $exception) { - $this->object->{$this->property} = $value; - } + $this->invadeSetter($this->object, $this->property, $value); if ($afterSetter = $this->afterSetter) { $afterSetter($value); } } - - /** - * Get reflected property. - * - * @return \ReflectionProperty - */ - protected function reflectedProperty() - { - $property = (new ReflectionObject($this->object))->getProperty($this->property); - $property->setAccessible(true); - - return $property; - } } diff --git a/src/Support/Traits/InvadesProperties.php b/src/Support/Traits/InvadesProperties.php new file mode 100644 index 00000000000..b654a0a9ec3 --- /dev/null +++ b/src/Support/Traits/InvadesProperties.php @@ -0,0 +1,34 @@ + $this->{$property})->call($object); + } + + return $property->call($object); + } + + protected function invadeSetter($object, $property, $value = null) + { + if (! $property) { + return; + } + + if (is_string($property) || ! is_callable($property)) { + (fn () => $this->{$property} = $value)->call($object); + + return; + } + + $property->call($object); + } +} From ca0fe9e87f8afe07284dc90395576e49df05c7a0 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 25 Feb 2024 14:27:57 -0600 Subject: [PATCH 10/81] Allows for entries to receive index items during hydration --- src/Data/ReceivesIndexValues.php | 77 +++++++++++++++ src/Entries/Entry.php | 24 ++++- src/Facades/Stache.php | 6 ++ src/Stache/Indexes/Index.php | 7 +- src/Stache/Stache.php | 96 +++++++++++++++++++ src/Stache/Stores/BasicStore.php | 19 +++- src/Stache/Stores/CollectionTreeStore.php | 8 ++ src/Stache/Stores/EntriesStore.php | 2 + tests/Antlers/Runtime/RuntimeValuesTest.php | 2 +- tests/Antlers/Runtime/TagCheckScopeTest.php | 4 +- .../Stores/AssetContainersStoreTest.php | 7 ++ 11 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 src/Data/ReceivesIndexValues.php diff --git a/src/Data/ReceivesIndexValues.php b/src/Data/ReceivesIndexValues.php new file mode 100644 index 00000000000..1616a6c6f68 --- /dev/null +++ b/src/Data/ReceivesIndexValues.php @@ -0,0 +1,77 @@ +indexedValues[$index] = $value; + } + + return $this; + } + + /** + * Get a value from the instance that was set from a Stache index. + * + * @param string $index The Stache index name. + * @return mixed + */ + protected function getIndexedValue(string $index) + { + if (! Stache::shouldUseIndexValues()) { + return null; + } + + return $this->indexedValues[$index] ?? null; + } + + /** + * Remove an indexed value from the instance. + * + * @param string $index The Stache index name. + * @return $this + */ + public function flushIndexedValue(string $index) + { + if (isset($this->indexedValues[$index])) { + unset($this->indexedValues[$index]); + } + + return $this; + } + + /** + * Remove all indexed values from the instance. + * + * @return $this + */ + public function flushIndexedValues() + { + $this->indexedValues = []; + + return $this; + } +} diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index fd76d84dd16..f7adcae2757 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -25,6 +25,7 @@ use Statamic\Data\HasAugmentedInstance; use Statamic\Data\HasOrigin; use Statamic\Data\Publishable; +use Statamic\Data\ReceivesIndexValues; use Statamic\Data\TracksLastModified; use Statamic\Data\TracksQueriedColumns; use Statamic\Data\TracksQueriedRelations; @@ -42,7 +43,6 @@ use Statamic\Facades\Collection; use Statamic\Facades\Site; use Statamic\Facades\Stache; -use Statamic\Fields\Value; use Statamic\GraphQL\ResolvesValues; use Statamic\Revisions\Revisable; use Statamic\Routing\Routable; @@ -54,7 +54,7 @@ class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract { - use ContainsComputedData, ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, Localizable, Publishable, Revisable, Searchable, TracksLastModified, TracksQueriedColumns, TracksQueriedRelations; + use ContainsComputedData, ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, Localizable, Publishable, ReceivesIndexValues, Revisable, Searchable, TracksLastModified, TracksQueriedColumns, TracksQueriedRelations; use HasOrigin { value as originValue; @@ -315,6 +315,8 @@ public function saveQuietly() public function save() { + $this->flushIndexedValues(); + $isNew = is_null(Facades\Entry::find($this->id())); $withEvents = $this->withEvents; @@ -522,6 +524,18 @@ public function date($date = null) ->args(func_get_args()); } + public function receivesIndexValues() + { + return ['uri']; + } + + public function getDependantIndexes() + { + return [ + 'entries' => ['uri'], + ]; + } + public function hasDate() { return $this->collection()->dated(); @@ -837,6 +851,12 @@ public function routeData() public function uri() { + $indexedUri = $this->getIndexedValue('uri'); + + if ($indexedUri !== null) { + return $indexedUri; + } + if (! $this->route()) { return null; } diff --git a/src/Facades/Stache.php b/src/Facades/Stache.php index 6a321cbd1a9..021b0d59014 100644 --- a/src/Facades/Stache.php +++ b/src/Facades/Stache.php @@ -24,6 +24,12 @@ * @method static mixed|null buildDate() * @method static self disableUpdatingIndexes() * @method static bool shouldUpdateIndexes() + * @method static bool shouldUseIndexValues() + * @method static self setShouldUseIndexValues($allowed = true) + * @method static self withoutIndexedValues(callable $callback) + * @method static void flushIndexValues($index) + * @method static void updateDependantIndexes($store, $handle) + * @method static void itemUsingIndexValues($index, $item) * * @see \Statamic\Stache\Stache */ diff --git a/src/Stache/Indexes/Index.php b/src/Stache/Indexes/Index.php index c147917cd06..eb3e57ed59a 100644 --- a/src/Stache/Indexes/Index.php +++ b/src/Stache/Indexes/Index.php @@ -92,9 +92,12 @@ public function update() debugbar()->addMessage("Updating index: {$this->store->key()}/{$this->name}", 'stache'); + Stache::flushIndexValues($this->name); + Stache::setShouldUseIndexValues(false); $this->items = $this->getItems(); $this->cache(); + Stache::setShouldUseIndexValues(true); return $this; } @@ -113,7 +116,9 @@ public function updateItem($item) { $this->load(); - $this->put($this->store->getItemKey($item), $this->getItemValue($item)); + Stache::withoutIndexedValues(function () use ($item) { + $this->put($this->store->getItemKey($item), $this->getItemValue($item)); + }); $this->cache(); } diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index 1bfab301e9f..bbacc8bb926 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Cache; use Statamic\Extensions\FileStore; use Statamic\Facades\File; +use Statamic\Stache\Stores\AggregateStore; use Statamic\Stache\Stores\Store; use Statamic\Support\Str; use Symfony\Component\Lock\LockFactory; @@ -21,12 +22,107 @@ class Stache protected $lockFactory; protected $locks = []; protected $duplicates; + protected $indexedValuesAllowed = true; + protected $indexReferences = []; + protected $dependantIndexClasses = []; + protected $dependantIndexes = []; public function __construct() { $this->stores = collect(); } + protected function registerDependantIndexes($item) + { + $class = get_class($item); + + if (array_key_exists($class, $this->dependantIndexClasses)) { + return; + } + + // Prevent registering the same class multiple times. + $this->dependantIndexClasses[$class] = true; + + $dependencies = $item->getDependantIndexes(); + + foreach ($dependencies as $store => $indexNames) { + if (! array_key_exists($store, $this->dependantIndexes)) { + $this->dependantIndexes[$store] = []; + } + + $this->dependantIndexes[$store] = array_merge($this->dependantIndexes[$store], $indexNames); + } + } + + public function updateDependantIndexes($store, $handle) + { + if (! array_key_exists($store, $this->dependantIndexes)) { + return; + } + + $this->withoutIndexedValues(function () use ($store, $handle) { + $storeInstance = $this->store($store); + foreach ($this->dependantIndexes[$store] as $index) { + if ($storeInstance instanceof AggregateStore) { + $storeInstance->store($handle)->index($index)->update(); + } else { + $storeInstance->index($index)->update(); + } + } + }); + } + + public function itemUsingIndexValues($index, $item) + { + $this->registerDependantIndexes($item); + + if (! array_key_exists($index, $this->indexReferences)) { + $this->indexReferences[$index] = []; + } + + $this->indexReferences[$index][] = $item; + } + + public function flushIndexValues($index) + { + if (! array_key_exists($index, $this->indexReferences)) { + return; + } + + foreach ($this->indexReferences[$index] as $item) { + if (! method_exists($item, 'flushIndexedValue')) { + continue; + } + + $item->flushIndexedValue($index); + } + } + + public function shouldUseIndexValues() + { + return $this->indexedValuesAllowed; + } + + public function setShouldUseIndexValues($allowed = true) + { + $this->indexedValuesAllowed = $allowed; + + return $this; + } + + public function withoutIndexedValues(callable $callback) + { + $currentSetting = $this->shouldUseIndexValues(); + + $this->setShouldUseIndexValues(false); + + $result = $callback(); + + $this->setShouldUseIndexValues($currentSetting); + + return $result; + } + public function sites($sites = null) { if (! $sites) { diff --git a/src/Stache/Stores/BasicStore.php b/src/Stache/Stores/BasicStore.php index f12eed78f09..63b28b0b2bf 100644 --- a/src/Stache/Stores/BasicStore.php +++ b/src/Stache/Stores/BasicStore.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Cache; use Statamic\Facades\File; +use Statamic\Facades\Stache; use Symfony\Component\Finder\SplFileInfo; abstract class BasicStore extends Store @@ -38,7 +39,23 @@ protected function getCachedItem($key) { $cacheKey = $this->getItemCacheKey($key); - return Cache::get($cacheKey); + $item = Cache::get($cacheKey); + + if ($item && method_exists($item, 'receivesIndexValues')) { + $id = $item->id(); + + foreach ($item->receivesIndexValues() as $index) { + Stache::itemUsingIndexValues($index, $item); + + $value = $this->resolveIndex($index)->get($id); + + if ($value) { + $item->withIndexedValue($index, $value); + } + } + } + + return $item; } protected function cacheItem($item) diff --git a/src/Stache/Stores/CollectionTreeStore.php b/src/Stache/Stores/CollectionTreeStore.php index 1caac172c08..cd9c7950418 100644 --- a/src/Stache/Stores/CollectionTreeStore.php +++ b/src/Stache/Stores/CollectionTreeStore.php @@ -4,6 +4,7 @@ use Statamic\Facades\Collection; use Statamic\Facades\Path; +use Statamic\Facades\Stache; use Statamic\Structures\CollectionTree; use Symfony\Component\Finder\SplFileInfo; @@ -38,4 +39,11 @@ protected function newTreeClassByPath($path) ->locale($site) ->handle($handle); } + + public function save($item) + { + parent::save($item); + + Stache::updateDependantIndexes('entries', $item->handle()); + } } diff --git a/src/Stache/Stores/EntriesStore.php b/src/Stache/Stores/EntriesStore.php index 3e5209b72f2..fc84c1e97ed 100644 --- a/src/Stache/Stores/EntriesStore.php +++ b/src/Stache/Stores/EntriesStore.php @@ -8,6 +8,8 @@ class EntriesStore extends AggregateStore { protected $childStore = CollectionEntriesStore::class; + protected $storeIndexes = ['uri']; + public function key() { return 'entries'; diff --git a/tests/Antlers/Runtime/RuntimeValuesTest.php b/tests/Antlers/Runtime/RuntimeValuesTest.php index 8ee8ebf8862..52e58f7dfe8 100644 --- a/tests/Antlers/Runtime/RuntimeValuesTest.php +++ b/tests/Antlers/Runtime/RuntimeValuesTest.php @@ -27,7 +27,7 @@ public function test_supplemented_values_are_not_cached() $template = <<<'EOT' {{ title }} -{{ dont_cache:me_please }}{{ foo }}{{ /dont_cache:me_please }} +{{ %dont_cache:me_please }}{{ foo }}{{ /%dont_cache:me_please }} EOT; $instance = (new class extends Tags diff --git a/tests/Antlers/Runtime/TagCheckScopeTest.php b/tests/Antlers/Runtime/TagCheckScopeTest.php index f58c5c035c3..cfd939b5ec9 100644 --- a/tests/Antlers/Runtime/TagCheckScopeTest.php +++ b/tests/Antlers/Runtime/TagCheckScopeTest.php @@ -146,11 +146,11 @@ public function index() })::register(); $template = <<<'EOT' -{{ just_a_tag }} +{{ %just_a_tag }} {{ replicator_field }} {{ partial:inner }} {{ /replicator_field }} -{{ /just_a_tag }} +{{ /%just_a_tag }} EOT; $partial = <<<'PARTIAL' {{ stuff }} diff --git a/tests/Stache/Stores/AssetContainersStoreTest.php b/tests/Stache/Stores/AssetContainersStoreTest.php index b3ea2404d9f..e74ca64e9c2 100644 --- a/tests/Stache/Stores/AssetContainersStoreTest.php +++ b/tests/Stache/Stores/AssetContainersStoreTest.php @@ -105,6 +105,13 @@ public function it_uses_the_handle_as_the_item_key() /** @test */ public function it_saves_to_disk() { + Facades\Stache::shouldReceive('flushIndexValues') + ->zeroOrMoreTimes(); + Facades\Stache::shouldReceive('setShouldUseIndexValues') + ->zeroOrMoreTimes(); + Facades\Stache::shouldReceive('withoutIndexedValues') + ->zeroOrMoreTimes(); + Facades\Stache::shouldReceive('store') ->with('asset-containers') ->andReturn($this->store); From e54f0f6aef511048b6d1d3361397e126c4dd936e Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 25 Feb 2024 18:54:09 -0600 Subject: [PATCH 11/81] Update AugmentedPage.php --- src/Structures/AugmentedPage.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Structures/AugmentedPage.php b/src/Structures/AugmentedPage.php index a3dd2d635dc..0d953dbd046 100644 --- a/src/Structures/AugmentedPage.php +++ b/src/Structures/AugmentedPage.php @@ -9,6 +9,7 @@ class AugmentedPage extends AugmentedEntry { protected $page; protected $hasEntry = false; + protected $fieldsCache = null; public function __construct($page) { @@ -57,16 +58,20 @@ protected function getFromData($key) protected function blueprintFields() { - $fields = ($pageBlueprint = $this->page->blueprint()) - ? $pageBlueprint->fields()->all() - : collect(); + if ($this->fieldsCache === null) { + $fields = ($pageBlueprint = $this->page->blueprint()) + ? $pageBlueprint->fields()->all() + : collect(); - if ($this->page !== $this->data) { - $entryFields = $this->data->blueprint()->fields()->all(); - $fields = $entryFields->merge($fields); + if ($this->page !== $this->data) { + $entryFields = $this->data->blueprint()->fields()->all(); + $fields = $entryFields->merge($fields); + } + + $this->fieldsCache = $fields; } - return $fields; + return $this->fieldsCache; } protected function id() From da835b6516596094d791a59d2bfaf69a17ef5c2f Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 26 Feb 2024 10:17:25 -0500 Subject: [PATCH 12/81] use guards to avoid changing indentation --- src/Assets/AugmentedAsset.php | 116 ++++++++++++++--------------- src/Auth/AugmentedUser.php | 18 ++--- src/Data/HasOrigin.php | 23 +++--- src/Entries/AugmentedEntry.php | 14 ++-- src/Globals/AugmentedVariables.php | 6 +- src/Structures/AugmentedPage.php | 24 +++--- src/Taxonomies/AugmentedTerm.php | 14 ++-- 7 files changed, 107 insertions(+), 108 deletions(-) diff --git a/src/Assets/AugmentedAsset.php b/src/Assets/AugmentedAsset.php index d54346b0b8c..39a3be72d9c 100644 --- a/src/Assets/AugmentedAsset.php +++ b/src/Assets/AugmentedAsset.php @@ -12,66 +12,66 @@ class AugmentedAsset extends AbstractAugmented public function keys() { - if (! $this->cachedKeys) { - $keys = $this->data->data()->keys() - ->merge($this->data->supplements()->keys()) - ->merge([ - 'id', - 'title', - 'path', - 'filename', - 'basename', - 'extension', - 'is_asset', - 'is_audio', - 'is_previewable', - 'is_image', - 'is_svg', - 'is_video', - 'blueprint', - 'edit_url', - 'container', - 'folder', - 'url', - 'permalink', - 'api_url', - ]); - - if ($this->data->exists()) { - $keys = $keys->merge([ - 'size', - 'size_bytes', - 'size_kilobytes', - 'size_megabytes', - 'size_gigabytes', - 'size_b', - 'size_kb', - 'size_mb', - 'size_gb', - 'last_modified', - 'last_modified_timestamp', - 'last_modified_instance', - 'focus', - 'has_focus', - 'focus_css', - 'height', - 'width', - 'orientation', - 'ratio', - 'mime_type', - 'duration', - 'duration_seconds', - 'duration_minutes', - 'duration_sec', - 'duration_min', - 'playtime', - ]); - } - - $this->cachedKeys = $keys->merge($this->blueprintFields()->keys())->unique()->all(); + if ($this->cachedKeys) { + return $this->cachedKeys; } - return $this->cachedKeys; + $keys = $this->data->data()->keys() + ->merge($this->data->supplements()->keys()) + ->merge([ + 'id', + 'title', + 'path', + 'filename', + 'basename', + 'extension', + 'is_asset', + 'is_audio', + 'is_previewable', + 'is_image', + 'is_svg', + 'is_video', + 'blueprint', + 'edit_url', + 'container', + 'folder', + 'url', + 'permalink', + 'api_url', + ]); + + if ($this->data->exists()) { + $keys = $keys->merge([ + 'size', + 'size_bytes', + 'size_kilobytes', + 'size_megabytes', + 'size_gigabytes', + 'size_b', + 'size_kb', + 'size_mb', + 'size_gb', + 'last_modified', + 'last_modified_timestamp', + 'last_modified_instance', + 'focus', + 'has_focus', + 'focus_css', + 'height', + 'width', + 'orientation', + 'ratio', + 'mime_type', + 'duration', + 'duration_seconds', + 'duration_minutes', + 'duration_sec', + 'duration_min', + 'playtime', + ]); + } + + return $this->cachedKeys = $keys->merge($this->blueprintFields()->keys())->unique()->all(); } protected function isAsset() diff --git a/src/Auth/AugmentedUser.php b/src/Auth/AugmentedUser.php index a59e832e133..297f1b9a5d2 100644 --- a/src/Auth/AugmentedUser.php +++ b/src/Auth/AugmentedUser.php @@ -15,17 +15,17 @@ class AugmentedUser extends AbstractAugmented public function keys() { - if (! $this->cachedKeys) { - $this->cachedKeys = $this->data->data()->keys() - ->merge(collect($this->data->supplements() ?? [])->keys()) - ->merge($this->commonKeys()) - ->merge($this->roleHandles()) - ->merge($this->groupHandles()) - ->merge($this->blueprintFields()->keys()) - ->unique()->sort()->values()->all(); + if ($this->cachedKeys) { + return $this->cachedKeys; } - return $this->cachedKeys; + return $this->cachedKeys = $this->data->data()->keys() + ->merge(collect($this->data->supplements() ?? [])->keys()) + ->merge($this->commonKeys()) + ->merge($this->roleHandles()) + ->merge($this->groupHandles()) + ->merge($this->blueprintFields()->keys()) + ->unique()->sort()->values()->all(); } private function commonKeys() diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index f54bde94ecd..47491651e1a 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -15,21 +15,20 @@ trait HasOrigin public function keys() { - if (! $this->cachedKeys) { - $originFallbackKeys = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues()->keys() : collect(); - - $originKeys = $this->hasOrigin() ? $this->origin()->keys() : collect(); + if ($this->cachedKeys) { + return $this->cachedKeys; + } + $originFallbackKeys = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues()->keys() : collect(); - $computedKeys = method_exists($this, 'computedKeys') ? $this->computedKeys() : []; + $originKeys = $this->hasOrigin() ? $this->origin()->keys() : collect(); - $this->cachedKeys = collect() - ->merge($originFallbackKeys) - ->merge($originKeys) - ->merge($this->data->keys()) - ->merge($computedKeys); - } + $computedKeys = method_exists($this, 'computedKeys') ? $this->computedKeys() : []; - return $this->cachedKeys; + return $this->cachedKeys = collect() + ->merge($originFallbackKeys) + ->merge($originKeys) + ->merge($this->data->keys()) + ->merge($computedKeys); } public function values() diff --git a/src/Entries/AugmentedEntry.php b/src/Entries/AugmentedEntry.php index 9cb7f76d830..6f7e46302ef 100644 --- a/src/Entries/AugmentedEntry.php +++ b/src/Entries/AugmentedEntry.php @@ -12,15 +12,15 @@ class AugmentedEntry extends AbstractAugmented public function keys() { - if (! $this->keysCache) { - $this->keysCache = $this->data->keys() - ->merge($this->data->supplements()->keys()) - ->merge($this->commonKeys()) - ->merge($this->blueprintFields()->keys()) - ->unique()->sort()->values()->all(); + if ($this->keysCache) { + return $this->keysCache; } - return $this->keysCache; + return $this->keysCache = $this->data->keys() + ->merge($this->data->supplements()->keys()) + ->merge($this->commonKeys()) + ->merge($this->blueprintFields()->keys()) + ->unique()->sort()->values()->all(); } private function commonKeys() diff --git a/src/Globals/AugmentedVariables.php b/src/Globals/AugmentedVariables.php index 63ede4006f5..39b9a5181ce 100644 --- a/src/Globals/AugmentedVariables.php +++ b/src/Globals/AugmentedVariables.php @@ -10,11 +10,11 @@ class AugmentedVariables extends AbstractAugmented public function keys() { - if (! $this->cachedKeys) { - $this->cachedKeys = $this->data->values()->keys()->all(); + if ($this->cachedKeys) { + return $this->cachedKeys; } - return $this->cachedKeys; + return $this->cachedKeys = $this->data->values()->keys()->all(); } public function site() diff --git a/src/Structures/AugmentedPage.php b/src/Structures/AugmentedPage.php index c9973557fc8..a5eb6987cdf 100644 --- a/src/Structures/AugmentedPage.php +++ b/src/Structures/AugmentedPage.php @@ -26,22 +26,22 @@ public function __construct($page) public function keys() { - if (! $this->cachedKeys) { - $keys = collect($this->hasEntry - ? parent::keys() - : ['title', 'url', 'uri', 'permalink', 'id']); + if ($this->cachedKeys) { + return $this->cachedKeys; + } - $keys = $keys - ->merge($this->page->data()->keys()) - ->merge($this->page->supplements()->keys()) - ->merge(['entry_id']); + $keys = collect($this->hasEntry + ? parent::keys() + : ['title', 'url', 'uri', 'permalink', 'id']); - $keys = Statamic::isApiRoute() ? $this->apiKeys($keys) : $keys; + $keys = $keys + ->merge($this->page->data()->keys()) + ->merge($this->page->supplements()->keys()) + ->merge(['entry_id']); - $this->cachedKeys = $keys->unique()->sort()->values()->all(); - } + $keys = Statamic::isApiRoute() ? $this->apiKeys($keys) : $keys; - return $this->cachedKeys; + return $this->cachedKeys = $keys->unique()->sort()->values()->all(); } private function apiKeys($keys) diff --git a/src/Taxonomies/AugmentedTerm.php b/src/Taxonomies/AugmentedTerm.php index 4ccf00ce428..13a89c14a24 100644 --- a/src/Taxonomies/AugmentedTerm.php +++ b/src/Taxonomies/AugmentedTerm.php @@ -12,15 +12,15 @@ class AugmentedTerm extends AbstractAugmented public function keys() { - if (! $this->cachedKeys) { - $this->cachedKeys = $this->data->values()->keys() - ->merge($this->data->supplements()->keys()) - ->merge($this->commonKeys()) - ->merge($this->blueprintFields()->keys()) - ->unique()->sort()->values()->all(); + if ($this->cachedKeys) { + return $this->cachedKeys; } - return $this->cachedKeys; + return $this->cachedKeys = $this->data->values()->keys() + ->merge($this->data->supplements()->keys()) + ->merge($this->commonKeys()) + ->merge($this->blueprintFields()->keys()) + ->unique()->sort()->values()->all(); } private function commonKeys() From a1ba023a561e4eda588a80001c3d05359e223ec8 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 26 Feb 2024 10:18:08 -0500 Subject: [PATCH 13/81] breathe --- src/Data/HasOrigin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 47491651e1a..1f28b87f2ec 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -18,6 +18,7 @@ public function keys() if ($this->cachedKeys) { return $this->cachedKeys; } + $originFallbackKeys = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues()->keys() : collect(); $originKeys = $this->hasOrigin() ? $this->origin()->keys() : collect(); From 1237cabb02ac05799c2523939285ebb23d507055 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 26 Feb 2024 10:20:33 -0500 Subject: [PATCH 14/81] use guard to avoid changing indentation --- src/Structures/AugmentedPage.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Structures/AugmentedPage.php b/src/Structures/AugmentedPage.php index 0d953dbd046..49a29166fec 100644 --- a/src/Structures/AugmentedPage.php +++ b/src/Structures/AugmentedPage.php @@ -58,20 +58,20 @@ protected function getFromData($key) protected function blueprintFields() { - if ($this->fieldsCache === null) { - $fields = ($pageBlueprint = $this->page->blueprint()) - ? $pageBlueprint->fields()->all() - : collect(); + if ($this->fieldsCache) { + return $this->fieldsCache; + } - if ($this->page !== $this->data) { - $entryFields = $this->data->blueprint()->fields()->all(); - $fields = $entryFields->merge($fields); - } + $fields = ($pageBlueprint = $this->page->blueprint()) + ? $pageBlueprint->fields()->all() + : collect(); - $this->fieldsCache = $fields; + if ($this->page !== $this->data) { + $entryFields = $this->data->blueprint()->fields()->all(); + $fields = $entryFields->merge($fields); } - return $this->fieldsCache; + return $this->fieldsCache = $fields; } protected function id() From 9cf6cd131ac6adf2e0a7ee6983879092c74578d3 Mon Sep 17 00:00:00 2001 From: John Koster Date: Mon, 26 Feb 2024 17:40:36 -0600 Subject: [PATCH 15/81] 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 16/81] 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(); From 284da4f4c0e42b6ba4eee765ae9bc6e7af2fd58d Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 27 Feb 2024 20:27:05 -0600 Subject: [PATCH 17/81] Refactor to spatie/invade --- composer.json | 1 + src/Support/FluentGetterSetter.php | 9 +++---- src/Support/Traits/InvadesProperties.php | 34 ------------------------ 3 files changed, 5 insertions(+), 39 deletions(-) delete mode 100644 src/Support/Traits/InvadesProperties.php diff --git a/composer.json b/composer.json index 8c867b6f05f..4dd34d0edbf 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "rebing/graphql-laravel": "^6.5 || ^8.0", "rhukster/dom-sanitizer": "^1.0.6", "spatie/blink": "^1.3", + "spatie/invade": "^2.0", "statamic/stringy": "^3.1.2", "symfony/http-foundation": "^4.3.3 || ^5.1.4 || ^6.0", "symfony/lock": "^5.4", diff --git a/src/Support/FluentGetterSetter.php b/src/Support/FluentGetterSetter.php index 3db29a2a0dc..0e5c8758515 100644 --- a/src/Support/FluentGetterSetter.php +++ b/src/Support/FluentGetterSetter.php @@ -3,17 +3,15 @@ namespace Statamic\Support; use Closure; -use Statamic\Support\Traits\InvadesProperties; class FluentGetterSetter { - use InvadesProperties; - protected $object; protected $property; protected $getter; protected $setter; protected $afterSetter; + protected $invader; /** * Instantiate fluent getter/setter helper. @@ -25,6 +23,7 @@ public function __construct($object, $property) { $this->object = $object; $this->property = $property; + $this->invader = invade($object); } /** @@ -106,7 +105,7 @@ public function args($args) */ protected function runGetterLogic() { - $value = $this->invade($this->object, $this->property); + $value = $this->invader->{$this->property}; if ($getter = $this->getter) { $value = $getter($value); @@ -126,7 +125,7 @@ protected function runSetterLogic($value) $value = $setter($value); } - $this->invadeSetter($this->object, $this->property, $value); + $this->invader->{$this->property} = $value; if ($afterSetter = $this->afterSetter) { $afterSetter($value); diff --git a/src/Support/Traits/InvadesProperties.php b/src/Support/Traits/InvadesProperties.php deleted file mode 100644 index b654a0a9ec3..00000000000 --- a/src/Support/Traits/InvadesProperties.php +++ /dev/null @@ -1,34 +0,0 @@ - $this->{$property})->call($object); - } - - return $property->call($object); - } - - protected function invadeSetter($object, $property, $value = null) - { - if (! $property) { - return; - } - - if (is_string($property) || ! is_callable($property)) { - (fn () => $this->{$property} = $value)->call($object); - - return; - } - - $property->call($object); - } -} From 6e68be214d724ef04758c7914ced1c4407a2a42e Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 27 Feb 2024 20:35:01 -0600 Subject: [PATCH 18/81] Revert "Refactor to spatie/invade" This reverts commit 284da4f4c0e42b6ba4eee765ae9bc6e7af2fd58d. --- composer.json | 1 - src/Support/FluentGetterSetter.php | 9 ++++--- src/Support/Traits/InvadesProperties.php | 34 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 src/Support/Traits/InvadesProperties.php diff --git a/composer.json b/composer.json index 4dd34d0edbf..8c867b6f05f 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,6 @@ "rebing/graphql-laravel": "^6.5 || ^8.0", "rhukster/dom-sanitizer": "^1.0.6", "spatie/blink": "^1.3", - "spatie/invade": "^2.0", "statamic/stringy": "^3.1.2", "symfony/http-foundation": "^4.3.3 || ^5.1.4 || ^6.0", "symfony/lock": "^5.4", diff --git a/src/Support/FluentGetterSetter.php b/src/Support/FluentGetterSetter.php index 0e5c8758515..3db29a2a0dc 100644 --- a/src/Support/FluentGetterSetter.php +++ b/src/Support/FluentGetterSetter.php @@ -3,15 +3,17 @@ namespace Statamic\Support; use Closure; +use Statamic\Support\Traits\InvadesProperties; class FluentGetterSetter { + use InvadesProperties; + protected $object; protected $property; protected $getter; protected $setter; protected $afterSetter; - protected $invader; /** * Instantiate fluent getter/setter helper. @@ -23,7 +25,6 @@ public function __construct($object, $property) { $this->object = $object; $this->property = $property; - $this->invader = invade($object); } /** @@ -105,7 +106,7 @@ public function args($args) */ protected function runGetterLogic() { - $value = $this->invader->{$this->property}; + $value = $this->invade($this->object, $this->property); if ($getter = $this->getter) { $value = $getter($value); @@ -125,7 +126,7 @@ protected function runSetterLogic($value) $value = $setter($value); } - $this->invader->{$this->property} = $value; + $this->invadeSetter($this->object, $this->property, $value); if ($afterSetter = $this->afterSetter) { $afterSetter($value); diff --git a/src/Support/Traits/InvadesProperties.php b/src/Support/Traits/InvadesProperties.php new file mode 100644 index 00000000000..b654a0a9ec3 --- /dev/null +++ b/src/Support/Traits/InvadesProperties.php @@ -0,0 +1,34 @@ + $this->{$property})->call($object); + } + + return $property->call($object); + } + + protected function invadeSetter($object, $property, $value = null) + { + if (! $property) { + return; + } + + if (is_string($property) || ! is_callable($property)) { + (fn () => $this->{$property} = $value)->call($object); + + return; + } + + $property->call($object); + } +} From e7eafa92c0414bddb21f605e6c11b47d3bd8820c Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 10:00:31 -0600 Subject: [PATCH 19/81] Delay the resolution of regular values --- src/Data/AbstractAugmented.php | 28 +++++++++-- src/Data/AugmentedCollection.php | 8 +++- src/Data/AugmentedData.php | 2 +- src/Data/DeferredValue.php | 66 ++++++++++++++++++++++++++ src/Structures/AugmentedPage.php | 2 +- tests/Data/AugmentedCollectionTest.php | 2 +- 6 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 src/Data/DeferredValue.php diff --git a/src/Data/AbstractAugmented.php b/src/Data/AbstractAugmented.php index 33c18e66bb8..a89de8b6646 100644 --- a/src/Data/AbstractAugmented.php +++ b/src/Data/AbstractAugmented.php @@ -73,14 +73,20 @@ public function get($handle, $fieldtype = null): Value $method = Str::camel($handle); if ($this->methodExistsOnThisClass($method)) { - return $this->wrapInvokable($method, true, $this, $handle, $fieldtype); + $value = $this->wrapInvokable($method, true, $this, $handle, $fieldtype); + } elseif (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) { + $value = $this->wrapInvokable($method, false, $this->data, $handle, $fieldtype); + } else { + $value = $this->wrapDeferredValue($handle, $fieldtype); } - if (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) { - return $this->wrapInvokable($method, false, $this->data, $handle, $fieldtype); + // If someone is calling ->get() directly they probably + // don't want to remember to also ->materialize() it. + if (! $this->isSelecting) { + return $value->materialize(); } - return $this->wrapValue($this->getFromData($handle), $handle, $fieldtype); + return $value; } protected function filterKeys($keys) @@ -100,7 +106,7 @@ private function methodExistsOnThisClass($method) return method_exists($this, $method) && ! in_array($method, ['select', 'except']); } - protected function getFromData($handle) + public function getFromData($handle) { $value = method_exists($this->data, 'value') ? $this->data->value($handle) : $this->data->get($handle); @@ -113,6 +119,18 @@ protected function getFromData($handle) return $value; } + protected function wrapDeferredValue($handle, $fieldtype = null) + { + $fieldtype = $this->adjustFieldtype($handle, $fieldtype); + + return (new DeferredValue( + null, + $handle, + $fieldtype, + $this->data + ))->withAugmentedReference($this); + } + protected function wrapInvokable(string $method, bool $proxy, $methodTarget, string $handle, $fieldtype = null) { $fieldtype = $this->adjustFieldtype($handle, $fieldtype); diff --git a/src/Data/AugmentedCollection.php b/src/Data/AugmentedCollection.php index 08c6aad9b59..a2659370299 100644 --- a/src/Data/AugmentedCollection.php +++ b/src/Data/AugmentedCollection.php @@ -47,10 +47,16 @@ public function withoutEvaluation() return $this; } + protected function requiresMaterialization($item) + { + return $item instanceof InvokableValue || + $item instanceof DeferredValue; + } + public function all() { return collect($this->items)->map(function ($item) { - if ($item instanceof InvokableValue) { + if ($this->requiresMaterialization($item)) { return $item->materialize(); } diff --git a/src/Data/AugmentedData.php b/src/Data/AugmentedData.php index 33ef668f39a..774852cb028 100644 --- a/src/Data/AugmentedData.php +++ b/src/Data/AugmentedData.php @@ -20,7 +20,7 @@ public function keys() return array_keys($this->array); } - protected function getFromData($handle) + public function getFromData($handle) { return $this->array[$handle] ?? null; } diff --git a/src/Data/DeferredValue.php b/src/Data/DeferredValue.php new file mode 100644 index 00000000000..40d0d6161f6 --- /dev/null +++ b/src/Data/DeferredValue.php @@ -0,0 +1,66 @@ +hasResolved) { + return; + } + + $this->hasResolved = true; + + if ($this->augmentedReference == null) { + return; + } + + $this->raw = $this->augmentedReference->getFromData($this->handle); + } + + public function withAugmentedReference($instance) + { + $this->augmentedReference = $instance; + + return $this; + } + + public function materialize() + { + $this->resolve(); + + 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/Structures/AugmentedPage.php b/src/Structures/AugmentedPage.php index a3dd2d635dc..3ebf8da4e79 100644 --- a/src/Structures/AugmentedPage.php +++ b/src/Structures/AugmentedPage.php @@ -46,7 +46,7 @@ private function apiKeys($keys) }); } - protected function getFromData($key) + public function getFromData($key) { if ($key === 'title') { return $this->page->title(); diff --git a/tests/Data/AugmentedCollectionTest.php b/tests/Data/AugmentedCollectionTest.php index c600791f0bd..a9527fb2443 100644 --- a/tests/Data/AugmentedCollectionTest.php +++ b/tests/Data/AugmentedCollectionTest.php @@ -40,7 +40,7 @@ public function values_get_flagged_shallow_when_calling_toArray_with_flag() $value = m::mock(Value::class); // $value->shouldNotReceive('toArray'); $value->shouldReceive('isRelationship')->andReturnFalse(); - $value->shouldReceive('shallow')->once()->andReturnSelf(); + $value->shouldReceive('shallow')->once()->andReturn($value); $c = new AugmentedCollection([$value]); $results = $c->withShallowNesting()->toArray(); From 7b7cfa442de2c7bfcda973a47e50e80977482091 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 10:27:04 -0600 Subject: [PATCH 20/81] Refactors shared logic across deferred values --- src/Data/Concerns/ResolvesValues.php | 43 ++++++++++++++++++++++++++++ src/Data/DeferredValue.php | 36 ++--------------------- src/Data/InvokableValue.php | 29 ++----------------- 3 files changed, 49 insertions(+), 59 deletions(-) create mode 100644 src/Data/Concerns/ResolvesValues.php diff --git a/src/Data/Concerns/ResolvesValues.php b/src/Data/Concerns/ResolvesValues.php new file mode 100644 index 00000000000..ab413953112 --- /dev/null +++ b/src/Data/Concerns/ResolvesValues.php @@ -0,0 +1,43 @@ +resolve(); + + 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/Data/DeferredValue.php b/src/Data/DeferredValue.php index 40d0d6161f6..aac8b403995 100644 --- a/src/Data/DeferredValue.php +++ b/src/Data/DeferredValue.php @@ -2,10 +2,13 @@ namespace Statamic\Data; +use Statamic\Data\Concerns\ResolvesValues; use Statamic\Fields\Value; class DeferredValue extends Value { + use ResolvesValues; + protected $augmentedReference = null; protected $hasResolved = false; @@ -30,37 +33,4 @@ public function withAugmentedReference($instance) return $this; } - - public function materialize() - { - $this->resolve(); - - 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/Data/InvokableValue.php b/src/Data/InvokableValue.php index 39828aeb456..7c3bd2adbcd 100644 --- a/src/Data/InvokableValue.php +++ b/src/Data/InvokableValue.php @@ -2,11 +2,14 @@ namespace Statamic\Data; +use Statamic\Data\Concerns\ResolvesValues; use Statamic\Fields\Value; use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState; class InvokableValue extends Value { + use ResolvesValues; + protected $methodTarget = null; protected bool $hasResolved = false; protected string $methodName; @@ -74,30 +77,4 @@ public function materialize() 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(); - } } From b10b6c5ec96b3f41a86bc3acb2ce4c57d071cce7 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 11:19:36 -0600 Subject: [PATCH 21/81] Need to account for relationships --- src/Data/Concerns/ResolvesValues.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Data/Concerns/ResolvesValues.php b/src/Data/Concerns/ResolvesValues.php index ab413953112..b54246bd621 100644 --- a/src/Data/Concerns/ResolvesValues.php +++ b/src/Data/Concerns/ResolvesValues.php @@ -40,4 +40,11 @@ public function shallow() return parent::shallow(); } + + public function isRelationship(): bool + { + $this->resolve(); + + return parent::isRelationship(); + } } From ee66acddda7078d3c8fe889714ab58446bcc533a Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 11:25:06 -0600 Subject: [PATCH 22/81] Adds transient values --- src/Data/AbstractAugmented.php | 2 +- src/Data/AugmentedCollection.php | 3 +- src/Data/TransientValue.php | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/Data/TransientValue.php diff --git a/src/Data/AbstractAugmented.php b/src/Data/AbstractAugmented.php index a89de8b6646..ac77e9b800f 100644 --- a/src/Data/AbstractAugmented.php +++ b/src/Data/AbstractAugmented.php @@ -40,7 +40,7 @@ public function select($keys = null) $this->isSelecting = true; foreach ($keys as $key) { - $arr[$key] = $this->get($key, optional($fields->get($key))->fieldtype()); + $arr[$key] = (new TransientValue(null, $key, null))->withAugmentationReferences($this, $fields->get($key)); } $this->isSelecting = false; diff --git a/src/Data/AugmentedCollection.php b/src/Data/AugmentedCollection.php index a2659370299..bcd88e95ff4 100644 --- a/src/Data/AugmentedCollection.php +++ b/src/Data/AugmentedCollection.php @@ -50,7 +50,8 @@ public function withoutEvaluation() protected function requiresMaterialization($item) { return $item instanceof InvokableValue || - $item instanceof DeferredValue; + $item instanceof DeferredValue || + $item instanceof TransientValue; } public function all() diff --git a/src/Data/TransientValue.php b/src/Data/TransientValue.php new file mode 100644 index 00000000000..cf899f2496b --- /dev/null +++ b/src/Data/TransientValue.php @@ -0,0 +1,47 @@ +augmentedReference = $augmentable; + $this->fieldReference = $field; + + return $this; + } + + protected function resolve() + { + if ($this->hasResolved) { + return; + } + + $this->hasResolved = true; + + if ($this->augmentedReference === null) { + return; + } + + // Calling ->get() directly will materialize any other deferred values for us. + $value = $this->augmentedReference->get($this->handle, $this->fieldReference?->fieldtype()); + + if ($value === null) { + return; + } + + $this->raw = $value->raw(); + $this->fieldtype = $value->fieldtype(); + $this->augmentable = $value->augmentable(); + } +} From eedbb88c1a9a236b2c6d992cba5218939c141d4a Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 12:08:46 -0600 Subject: [PATCH 23/81] Refactor select to allow for known fields to be supplied --- src/Data/AbstractAugmented.php | 7 +++++-- src/Data/HasAugmentedInstance.php | 8 ++++---- tests/Data/HasAugmentedInstanceTest.php | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Data/AbstractAugmented.php b/src/Data/AbstractAugmented.php index ac77e9b800f..e0520e19273 100644 --- a/src/Data/AbstractAugmented.php +++ b/src/Data/AbstractAugmented.php @@ -30,12 +30,15 @@ public function except($keys) return $this->select(array_diff($this->keys(), Arr::wrap($keys))); } - public function select($keys = null) + public function select($keys = null, $fields = null) { $arr = []; + if (! $fields) { + $fields = $this->blueprintFields(); + } + $keys = $this->filterKeys(Arr::wrap($keys ?: $this->keys())); - $fields = $this->blueprintFields(); $this->isSelecting = true; diff --git a/src/Data/HasAugmentedInstance.php b/src/Data/HasAugmentedInstance.php index ed3871a1da6..8a7dbb2066a 100644 --- a/src/Data/HasAugmentedInstance.php +++ b/src/Data/HasAugmentedInstance.php @@ -14,11 +14,11 @@ public function augmentedValue($key) return $this->augmented()->get($key); } - public function toAugmentedCollection($keys = null) + public function toAugmentedCollection($keys = null, $fields = null) { return $this->augmented() ->withRelations($this->defaultAugmentedRelations()) - ->select($keys ?? $this->defaultAugmentedArrayKeys()); + ->select($keys ?? $this->defaultAugmentedArrayKeys(), $fields); } public function toAugmentedArray($keys = null) @@ -26,9 +26,9 @@ public function toAugmentedArray($keys = null) return $this->toAugmentedCollection($keys)->all(); } - public function toDeferredAugmentedArray($keys = null) + public function toDeferredAugmentedArray($keys = null, $fields = null) { - return $this->toAugmentedCollection($keys)->deferredAll(); + return $this->toAugmentedCollection($keys, $fields)->deferredAll(); } public function toShallowAugmentedCollection() diff --git a/tests/Data/HasAugmentedInstanceTest.php b/tests/Data/HasAugmentedInstanceTest.php index 2c5bd7b0df6..4165e500af8 100644 --- a/tests/Data/HasAugmentedInstanceTest.php +++ b/tests/Data/HasAugmentedInstanceTest.php @@ -21,8 +21,8 @@ public function it_makes_an_augmented_instance() $mock = $this->mock(Augmented::class); $mock->shouldReceive('withRelations')->with([])->andReturnSelf(); $mock->shouldReceive('get')->with('foo')->once()->andReturn(new Value('bar')); - $mock->shouldReceive('select')->with(null)->times(2)->andReturn($augmentedCollection); - $mock->shouldReceive('select')->with(['one'])->times(2)->andReturn($filteredAugmentedCollection); + $mock->shouldReceive('select')->with(null, null)->times(2)->andReturn($augmentedCollection); + $mock->shouldReceive('select')->with(['one'], null)->times(2)->andReturn($filteredAugmentedCollection); $mock->shouldReceive('select')->with(['id', 'title', 'api_url'])->times(1)->andReturn($shallowFilteredAugmentedCollection); $thing = new class($mock) @@ -62,7 +62,7 @@ public function augmented_thing_can_define_the_default_array_keys() { $mock = $this->mock(Augmented::class); $mock->shouldReceive('withRelations')->with([])->andReturnSelf(); - $mock->shouldReceive('select')->with(['foo', 'bar'])->once()->andReturn(new AugmentedCollection(['foo', 'bar'])); + $mock->shouldReceive('select')->with(['foo', 'bar'], null)->once()->andReturn(new AugmentedCollection(['foo', 'bar'])); $thing = new class($mock) { @@ -94,7 +94,7 @@ public function augmented_thing_can_define_the_default_relations() { $mock = $this->mock(Augmented::class); $mock->shouldReceive('withRelations')->with(['baz', 'qux'])->andReturnSelf(); - $mock->shouldReceive('select')->with(null)->once()->andReturn(new AugmentedCollection(['foo', 'bar'])); + $mock->shouldReceive('select')->with(null, null)->once()->andReturn(new AugmentedCollection(['foo', 'bar'])); $thing = new class($mock) { From 86d22d31bcfc6b58d14590c9b14d11fa2a8cd952 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 12:46:10 -0600 Subject: [PATCH 24/81] Adds the Augmentor 50000; uses it inside Antlers augmentation --- src/Contracts/Data/BulkAugmentable.php | 8 ++ src/Data/AbstractAugmented.php | 2 +- src/Data/BulkAugmentor.php | 95 +++++++++++++++++++ src/Entries/Entry.php | 14 ++- src/Structures/AugmentedPage.php | 2 +- src/Structures/Page.php | 19 +++- .../Runtime/Sandbox/RuntimeValues.php | 9 +- 7 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 src/Contracts/Data/BulkAugmentable.php create mode 100644 src/Data/BulkAugmentor.php diff --git a/src/Contracts/Data/BulkAugmentable.php b/src/Contracts/Data/BulkAugmentable.php new file mode 100644 index 00000000000..65297657280 --- /dev/null +++ b/src/Contracts/Data/BulkAugmentable.php @@ -0,0 +1,8 @@ +blueprintFields()->get($handle))->fieldtype(); } - protected function blueprintFields() + public function blueprintFields() { if (! isset($this->blueprintFields)) { $this->blueprintFields = (method_exists($this->data, 'blueprint') && $blueprint = $this->data->blueprint()) diff --git a/src/Data/BulkAugmentor.php b/src/Data/BulkAugmentor.php new file mode 100644 index 00000000000..1cbc13dfdad --- /dev/null +++ b/src/Data/BulkAugmentor.php @@ -0,0 +1,95 @@ +getAugmentationReferenceKey(); + } + + return 'Ref::'.get_class($item).spl_object_hash($item); + } + + public function augment($items) + { + $count = count($items); + + $referenceKeys = []; + $referenceFields = []; + + for ($i = 0; $i < $count; $i++) { + $item = $items[$i]; + $reference = $this->getAugmentationReference($item); + + if (! $this->isTree) { + $this->originalValues[$i] = $item; + } + + if (array_key_exists($reference, $referenceKeys)) { + continue; + } + + $referenceKeys[$reference] = $item->augmented()->keys(); + $referenceFields[$reference] = $item->augmented()->blueprintFields(); + } + + for ($i = 0; $i < $count; $i++) { + $item = $items[$i]; + $reference = $this->getAugmentationReference($item); + $fields = $referenceFields[$reference]; + $keys = $referenceKeys[$reference]; + + $this->augmentedValues[$i] = $item->toDeferredAugmentedArray($keys, $fields); + } + + return $this; + } + + public function augmentTree($tree) + { + $this->isTree = true; + + if (! $tree) { + return $this; + } + + $items = []; + + for ($i = 0; $i < count($tree); $i++) { + $item = $tree[$i]; + + $items[] = $item['page']; + $this->originalValues[$i] = $item; + } + + return $this->augment($items); + } + + public function map(callable $callable) + { + $items = []; + + for ($i = 0; $i < count($this->originalValues); $i++) { + $original = $this->originalValues[$i]; + $augmented = $this->augmentedValues[$i]; + + $items[] = call_user_func_array($callable, [$original, $augmented, $i]); + } + + return collect($items); + } + + public function augmented() + { + return $this->augmentedValues; + } +} diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index fd76d84dd16..590cbfa8ec4 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -13,6 +13,7 @@ use Statamic\Contracts\Auth\Protect\Protectable; use Statamic\Contracts\Data\Augmentable; use Statamic\Contracts\Data\Augmented; +use Statamic\Contracts\Data\BulkAugmentable; use Statamic\Contracts\Data\Localization; use Statamic\Contracts\Entries\Entry as Contract; use Statamic\Contracts\Entries\EntryRepository; @@ -42,7 +43,6 @@ use Statamic\Facades\Collection; use Statamic\Facades\Site; use Statamic\Facades\Stache; -use Statamic\Fields\Value; use Statamic\GraphQL\ResolvesValues; use Statamic\Revisions\Revisable; use Statamic\Routing\Routable; @@ -52,7 +52,7 @@ use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract +class Entry implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract { use ContainsComputedData, ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, Localizable, Publishable, Revisable, Searchable, TracksLastModified, TracksQueriedColumns, TracksQueriedRelations; @@ -76,6 +76,7 @@ class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableVal protected $withEvents = true; protected $template; protected $layout; + protected $augmentationReferenceKey; public function __construct() { @@ -88,6 +89,15 @@ public function id($id = null) return $this->fluentlyGetOrSet('id')->args(func_get_args()); } + public function getAugmentationReferenceKey(): string + { + if ($this->augmentationReferenceKey) { + return $this->augmentationReferenceKey; + } + + return $this->augmentationReferenceKey = 'Entry::'.$this->blueprint()->namespace(); + } + public function locale($locale = null) { return $this diff --git a/src/Structures/AugmentedPage.php b/src/Structures/AugmentedPage.php index 3ebf8da4e79..3a440f1a60a 100644 --- a/src/Structures/AugmentedPage.php +++ b/src/Structures/AugmentedPage.php @@ -55,7 +55,7 @@ public function getFromData($key) return $this->page->getSupplement($key) ?? $this->page->value($key); } - protected function blueprintFields() + public function blueprintFields() { $fields = ($pageBlueprint = $this->page->blueprint()) ? $pageBlueprint->fields()->all() diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 7455bd64d38..e74125e0b2d 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -10,6 +10,7 @@ use Statamic\Contracts\Auth\Protect\Protectable; use Statamic\Contracts\Data\Augmentable; use Statamic\Contracts\Data\Augmented; +use Statamic\Contracts\Data\BulkAugmentable; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\GraphQL\ResolvesValues as ResolvesValuesContract; use Statamic\Contracts\Routing\UrlBuilder; @@ -24,7 +25,7 @@ use Statamic\GraphQL\ResolvesValues; use Statamic\Support\Str; -class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializable, Protectable, ResolvesValuesContract, Responsable +class Page implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, Entry, JsonSerializable, Protectable, ResolvesValuesContract, Responsable { use ContainsSupplementalData, ForwardsCalls, HasAugmentedInstance, ResolvesValues, TracksQueriedColumns; @@ -39,12 +40,28 @@ class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializab protected $title; protected $depth; protected $data = []; + protected $augmentationReferenceKey; public function __construct() { $this->supplements = collect(); } + public function getAugmentationReferenceKey(): string + { + if ($this->augmentationReferenceKey) { + return $this->augmentationReferenceKey; + } + + $this->augmentationReferenceKey = 'Page::'; + + if ($entry = $this->entry()) { + $this->augmentationReferenceKey .= $entry->getAugmentationReferenceKey(); + } + + return $this->augmentationReferenceKey; + } + public function setUrl($url) { $this->url = $url; diff --git a/src/View/Antlers/Language/Runtime/Sandbox/RuntimeValues.php b/src/View/Antlers/Language/Runtime/Sandbox/RuntimeValues.php index 6d8b348c313..e3fc727fe59 100644 --- a/src/View/Antlers/Language/Runtime/Sandbox/RuntimeValues.php +++ b/src/View/Antlers/Language/Runtime/Sandbox/RuntimeValues.php @@ -3,6 +3,9 @@ namespace Statamic\View\Antlers\Language\Runtime\Sandbox; use Exception; +use Illuminate\Support\Collection; +use Statamic\Contracts\Data\Augmentable; +use Statamic\Data\BulkAugmentor; use Statamic\Fields\Value; use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState; @@ -12,7 +15,11 @@ public static function resolveWithRuntimeIsolation($augmentable) { GlobalRuntimeState::$requiresRuntimeIsolation = true; try { - $value = $augmentable->toDeferredAugmentedArray(); + if ($augmentable instanceof Collection && $augmentable->count() && $augmentable[0] instanceof Augmentable) { + $value = (new BulkAugmentor())->augment($augmentable)->augmented(); + } else { + $value = $augmentable->toDeferredAugmentedArray(); + } } catch (Exception $e) { throw $e; } finally { From 24d7878be3bb317ee52c8677913f01ee1486e62f Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 13:45:25 -0600 Subject: [PATCH 25/81] Some key adjustments * Allow for implementors to return `null`, which disables the bulk process for that item * Entry now takes data keys into consideration to prevent "locking" an entry into something if it has custom data --- src/Contracts/Data/BulkAugmentable.php | 2 +- src/Data/BulkAugmentor.php | 4 ++-- src/Entries/Entry.php | 6 ++++-- src/Structures/Page.php | 11 ++++++----- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Contracts/Data/BulkAugmentable.php b/src/Contracts/Data/BulkAugmentable.php index 65297657280..add62ea0204 100644 --- a/src/Contracts/Data/BulkAugmentable.php +++ b/src/Contracts/Data/BulkAugmentable.php @@ -4,5 +4,5 @@ interface BulkAugmentable { - public function getAugmentationReferenceKey(): string; + public function getAugmentationReferenceKey(): ?string; } diff --git a/src/Data/BulkAugmentor.php b/src/Data/BulkAugmentor.php index 1cbc13dfdad..97561531f6d 100644 --- a/src/Data/BulkAugmentor.php +++ b/src/Data/BulkAugmentor.php @@ -12,8 +12,8 @@ class BulkAugmentor protected function getAugmentationReference($item) { - if ($item instanceof BulkAugmentable) { - return $item->getAugmentationReferenceKey(); + if ($item instanceof BulkAugmentable && $key = $item->getAugmentationReferenceKey()) { + return $key; } return 'Ref::'.get_class($item).spl_object_hash($item); diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 590cbfa8ec4..d9b7828c90c 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -89,13 +89,15 @@ public function id($id = null) return $this->fluentlyGetOrSet('id')->args(func_get_args()); } - public function getAugmentationReferenceKey(): string + public function getAugmentationReferenceKey(): ?string { if ($this->augmentationReferenceKey) { return $this->augmentationReferenceKey; } - return $this->augmentationReferenceKey = 'Entry::'.$this->blueprint()->namespace(); + $dataPart = implode('|', $this->data->keys()->sort()->all()); + + return $this->augmentationReferenceKey = 'Entry::'.$this->blueprint()->namespace().'::'.$dataPart; } public function locale($locale = null) diff --git a/src/Structures/Page.php b/src/Structures/Page.php index e74125e0b2d..d691e925057 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -41,25 +41,26 @@ class Page implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, Entr protected $depth; protected $data = []; protected $augmentationReferenceKey; + protected $setAugmentationReferenceKey = false; public function __construct() { $this->supplements = collect(); } - public function getAugmentationReferenceKey(): string + public function getAugmentationReferenceKey(): ?string { - if ($this->augmentationReferenceKey) { + if ($this->setAugmentationReferenceKey) { return $this->augmentationReferenceKey; } - $this->augmentationReferenceKey = 'Page::'; + $this->setAugmentationReferenceKey = true; if ($entry = $this->entry()) { - $this->augmentationReferenceKey .= $entry->getAugmentationReferenceKey(); + return $this->augmentationReferenceKey = 'Page::'.$entry->getAugmentationReferenceKey(); } - return $this->augmentationReferenceKey; + return $this->augmentationReferenceKey = 'Page::'; } public function setUrl($url) From 477541afbe0bfc6b798392031fa1e9238c288a60 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 13:47:15 -0600 Subject: [PATCH 26/81] Update nav tag to use bulk augmentor --- src/Tags/Structure.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Tags/Structure.php b/src/Tags/Structure.php index 548f35c1c88..a9080fa8a0e 100644 --- a/src/Tags/Structure.php +++ b/src/Tags/Structure.php @@ -3,6 +3,7 @@ namespace Statamic\Tags; use Statamic\Contracts\Structures\Structure as StructureContract; +use Statamic\Data\BulkAugmentor; use Statamic\Exceptions\CollectionNotFoundException; use Statamic\Exceptions\NavigationNotFoundException; use Statamic\Facades\Collection; @@ -120,11 +121,9 @@ protected function isQueryingStatus() public function toArray($tree, $parent = null, $depth = 1) { - $pages = collect($tree)->map(function ($item, $index) use ($parent, $depth, $tree) { + $pages = (new BulkAugmentor())->augmentTree($tree)->map(function ($item, $data, $index) use ($depth, $tree, $parent) { $page = $item['page']; - $keys = $this->getQuerySelectKeys($page); - $data = $page->toDeferredAugmentedArray($keys); - $children = empty($item['children']) ? [] : $this->toArray($item['children'], $data, $depth + 1); + $children = empty($item['children']) ? [] : $this->toArray($item['children'], $page, $depth + 1); $url = $page->urlWithoutRedirect(); $absoluteUrl = $page->absoluteUrl(); From 2ede1a26d2160f4944c4c4b4ef4a5f60ed673bc8 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 15:15:51 -0600 Subject: [PATCH 27/81] Restore a familiar experience to dump, dd, and its friends --- src/Modifiers/CoreModifiers.php | 9 ++++---- src/Support/Dumper.php | 39 +++++++++++++++++++++++++++++++++ src/Tags/Dump.php | 6 +++-- 3 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/Support/Dumper.php diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index 10798eebf59..8f037372fce 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -27,6 +27,7 @@ use Statamic\Fieldtypes\Bard; use Statamic\Fieldtypes\Bard\Augmentor; use Statamic\Support\Arr; +use Statamic\Support\Dumper; use Statamic\Support\Html; use Statamic\Support\Str; use Stringy\StaticStringy as Stringy; @@ -487,7 +488,7 @@ public function daysAgo($value, $params) */ public function ddd($value) { - ddd($value); + ddd(Dumper::materializeValues($value)); } /** @@ -495,7 +496,7 @@ public function ddd($value) */ public function debug($value) { - debug($value); + debug(Dumper::materializeValues($value)); } /** @@ -544,7 +545,7 @@ public function dl($value, $params) */ public function dd($value) { - function_exists('ddd') ? ddd($value) : dd($value); + Dumper::dd($value); } /** @@ -552,7 +553,7 @@ function_exists('ddd') ? ddd($value) : dd($value); */ public function dump($value) { - dump($value); + Dumper::dump($value); } /** diff --git a/src/Support/Dumper.php b/src/Support/Dumper.php new file mode 100644 index 00000000000..df06c4c1365 --- /dev/null +++ b/src/Support/Dumper.php @@ -0,0 +1,39 @@ +mapWithKeys(function ($value, $key) { + if ($value instanceof Value) { + $value = $value->materialize(); + } + + return [$key => $value]; + })->all(); + } + + return $values; + } + + public static function dump($values) + { + dump(self::materializeValues($values)); + } + + public static function dd($values) + { + $values = self::materializeValues($values); + + function_exists('ddd') ? ddd($values) : dd($values); + } +} diff --git a/src/Tags/Dump.php b/src/Tags/Dump.php index 80827e453d4..1e9382bb14b 100644 --- a/src/Tags/Dump.php +++ b/src/Tags/Dump.php @@ -2,6 +2,8 @@ namespace Statamic\Tags; +use Statamic\Support\Dumper; + class Dump extends Tags { /** @@ -9,7 +11,7 @@ class Dump extends Tags */ public function index() { - dump($this->context->except(['__env', 'app'])->sortKeys()->all()); + Dumper::dump($this->context->except(['__env', 'app'])->sortKeys()->all()); } /** @@ -17,6 +19,6 @@ public function index() */ public function wildcard($var) { - dump($this->context->value($var)); + Dumper::dump($this->context->value($var)); } } From c3be9009c700f9782bc4a1e73626ac45955772da Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 15:41:35 -0600 Subject: [PATCH 28/81] Move string functions used in hot code paths to Str utility class --- src/Support/Str.php | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Support/Str.php b/src/Support/Str.php index 9296cdc3a6d..0ea05a99513 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -279,6 +279,30 @@ public static function safeTruncateReverse($string, $length, $substring = '') return IlluminateStr::reverse(StaticStringy::safeTruncate(IlluminateStr::reverse($string), $length, $substring)); } + public static function removeRight($string, $cap) + { + if (str_ends_with($string, $cap)) { + return mb_substr($string, 0, mb_strlen($string) - mb_strlen($cap)); + } + + return $string; + } + + public static function collapseWhitespace($string) + { + return trim(\mb_ereg_replace('[[:space:]]+', '', $string, 'msr')); + } + + public static function ensureLeft($string, $start) + { + return IlluminateStr::start($string, $start); + } + + public static function ensureRight($string, $cap) + { + return IlluminateStr::finish($string, $cap); + } + /** * Implicitly defer all other method calls to either \Stringy\StaticStringy or \Illuminate\Support\Str. * @@ -289,13 +313,13 @@ public static function safeTruncateReverse($string, $length, $substring = '') public static function __callStatic($method, $args) { $deferToStringy = [ - 'append', 'at', 'camelize', 'chars', 'collapseWhitespace', 'containsAny', 'count', 'countSubstr', - 'dasherize', 'delimit', 'endsWithAny', 'ensureLeft', 'ensureRight', 'first', 'getEncoding', 'getIterator', + 'append', 'at', 'camelize', 'chars', 'containsAny', 'count', 'countSubstr', + 'dasherize', 'delimit', 'endsWithAny', 'first', 'getEncoding', 'getIterator', 'hasLowerCase', 'hasUpperCase', 'htmlDecode', 'htmlEncode', 'humanize', 'indexOf', 'indexOfLast', 'insert', 'isAlpha', 'isAlphanumeric', 'isBase64', 'isBlank', 'isHexadecimal', 'isLowerCase', 'isSerialized', 'isUpperCase', 'last', 'lines', 'longestCommonPrefix', 'longestCommonSubstring', 'longestCommonSuffix', 'lowerCaseFirst', 'offsetExists', 'offsetGet', 'offsetSet', 'offsetUnset', 'pad', 'prepend', 'regexReplace', - 'removeLeft', 'removeRight', 'safeTruncate', 'shuffle', 'slice', 'slugify', 'split', 'startsWithAny', + 'removeLeft', 'safeTruncate', 'shuffle', 'slice', 'slugify', 'split', 'startsWithAny', 'stripWhitespace', 'surround', 'swapCase', 'tidy', 'titleize', 'toAscii', 'toBoolean', 'toLowerCase', 'toSpaces', 'toTabs', 'toTitleCase', 'toUpperCase', 'trim', 'trimLeft', 'trimRight', 'truncate', 'underscored', 'upperCamelize', 'upperCaseFirst', From 2529a062db02f3df1e2ff78aa4c864e8abca686c Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 16:00:34 -0600 Subject: [PATCH 29/81] Cache the `isApiRoute` results for the request --- src/Statamic.php | 14 ++++++++++++-- src/View/State/ClearState.php | 3 +++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Statamic.php b/src/Statamic.php index 24d5a968529..910d6db10c7 100644 --- a/src/Statamic.php +++ b/src/Statamic.php @@ -35,6 +35,7 @@ class Statamic protected static $jsonVariables = []; protected static $bootedCallbacks = []; protected static $afterInstalledCallbacks = []; + protected static $isApiRouteCache; public static function version() { @@ -211,13 +212,22 @@ public static function cpRoute($route, $params = []) return $route; } + public static function clearApiRouteCache() + { + self::$isApiRouteCache = null; + } + public static function isApiRoute() { + if (self::$isApiRouteCache !== null) { + return self::$isApiRouteCache; + } + if (! config('statamic.api.enabled') || ! static::pro()) { - return false; + return self::$isApiRouteCache = false; } - return starts_with(request()->path(), config('statamic.api.route')); + return self::$isApiRouteCache = starts_with(request()->path(), config('statamic.api.route')); } public static function apiRoute($route, $params = []) diff --git a/src/View/State/ClearState.php b/src/View/State/ClearState.php index 8ca8ef86ca9..486a927bdae 100644 --- a/src/View/State/ClearState.php +++ b/src/View/State/ClearState.php @@ -2,10 +2,13 @@ namespace Statamic\View\State; +use Statamic\Statamic; + class ClearState { public function handle() { + Statamic::clearApiRouteCache(); StateManager::resetState(); } } From 814fc4dd6a20d843f6b5a45a0cd0ce51c6fd5ba2 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 16:24:40 -0600 Subject: [PATCH 30/81] Removes fluent getter/setter and caches results for origin and hasOrigin --- src/Data/HasOrigin.php | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 186299cfad9..e2cbf854b54 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -10,6 +10,7 @@ trait HasOrigin * @var string */ protected $origin; + protected $cachedHasOrigin = false; public function keys() { @@ -58,18 +59,18 @@ public function value($key) public function origin($origin = null) { - return $this->fluentlyGetOrSet('origin') - ->getter(function ($origin) { - return $origin - ? Blink::once($this->getOriginBlinkKey(), fn () => $this->getOriginByString($origin)) - : null; - }) - ->setter(function ($origin) { - Blink::forget($this->getOriginBlinkKey()); - - return is_object($origin) ? $this->getOriginIdFromObject($origin) : $origin; - }) - ->args(func_get_args()); + if (func_num_args() === 0) { + return $this->origin + ? Blink::once($this->getOriginBlinkKey(), fn () => $this->getOriginByString($this->origin)) + : null; + } + + Blink::forget($this->getOriginBlinkKey()); + + $this->origin = is_object($origin) ? $this->getOriginIdFromObject($origin) : $origin; + $this->cachedHasOrigin = $this->origin != null; + + return $this; } abstract public function getOriginByString($origin); @@ -86,7 +87,11 @@ protected function getOriginIdFromObject($origin) public function hasOrigin() { - return $this->origin() !== null; + if (! $this->cachedHasOrigin && $this->origin) { + $this->cachedHasOrigin = true; + } + + return $this->cachedHasOrigin; } public function isRoot() From 3a69edc7adb80009299857e6ff30eb053ed4c486 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 16:33:26 -0600 Subject: [PATCH 31/81] Update StoresScopedComputedFieldCallbacks.php --- src/Data/StoresScopedComputedFieldCallbacks.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Data/StoresScopedComputedFieldCallbacks.php b/src/Data/StoresScopedComputedFieldCallbacks.php index 7d8025ce9bb..142e9141b0c 100644 --- a/src/Data/StoresScopedComputedFieldCallbacks.php +++ b/src/Data/StoresScopedComputedFieldCallbacks.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Support\Collection; +use Statamic\Facades\Blink; use Statamic\Support\Arr; use Statamic\Support\Str; @@ -23,8 +24,10 @@ public function computed($scopes, string $field, Closure $callback) public function getComputedCallbacks(string $scope): Collection { - return collect($this->computedFieldCallbacks) - ->filter(fn ($_, $key) => Str::startsWith($key, "{$scope}.")) - ->keyBy(fn ($_, $key) => Str::after($key, "{$scope}.")); + return Blink::once('getComputedCallbacks'.$scope, function () use ($scope) { + return collect($this->computedFieldCallbacks) + ->filter(fn ($_, $key) => Str::startsWith($key, "{$scope}.")) + ->keyBy(fn ($_, $key) => Str::after($key, "{$scope}.")); + }); } } From 49ddfd0aa14b89c42df55bcd85eea7e88c2b65d8 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 17:23:58 -0600 Subject: [PATCH 32/81] Cache computed routes on collection instance --- src/Entries/Collection.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Entries/Collection.php b/src/Entries/Collection.php index a96358a0748..86945599627 100644 --- a/src/Entries/Collection.php +++ b/src/Entries/Collection.php @@ -38,6 +38,7 @@ class Collection implements Arrayable, ArrayAccess, AugmentableContract, Contrac protected $handle; protected $routes = []; + protected $cachedRoutes = null; protected $mount; protected $title; protected $template; @@ -75,7 +76,13 @@ public function id() public function handle($handle = null) { - return $this->fluentlyGetOrSet('handle')->args(func_get_args()); + if ($handle === null) { + return $this->handle; + } + + $this->handle = $handle; + + return $this; } public function routes($routes = null) @@ -83,11 +90,17 @@ public function routes($routes = null) return $this ->fluentlyGetOrSet('routes') ->getter(function ($routes) { - return $this->sites()->mapWithKeys(function ($site) use ($routes) { + if ($this->cachedRoutes !== null) { + return $this->cachedRoutes; + } + + return $this->cachedRoutes = $this->sites()->mapWithKeys(function ($site) use ($routes) { $siteRoute = is_string($routes) ? $routes : ($routes[$site] ?? null); return [$site => $siteRoute]; }); + })->afterSetter(function () { + $this->cachedRoutes = null; }) ->args(func_get_args()); } @@ -389,6 +402,9 @@ public function sites($sites = null) return collect($sites); }) + ->afterSetter(function () { + $this->cachedRoutes = null; + }) ->args(func_get_args()); } From b66fad125b595bc8fe81bd5416643c5e746cdb0f Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 17:28:57 -0600 Subject: [PATCH 33/81] Update Collection.php --- src/Entries/Collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entries/Collection.php b/src/Entries/Collection.php index 86945599627..9ed2732d3ad 100644 --- a/src/Entries/Collection.php +++ b/src/Entries/Collection.php @@ -76,7 +76,7 @@ public function id() public function handle($handle = null) { - if ($handle === null) { + if (func_num_args() === 0) { return $this->handle; } From 3f30d71e9d87f1198a3db3fec4184f1d3fc3af3b Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 17:49:03 -0600 Subject: [PATCH 34/81] Cache collection instance on Entry --- src/Entries/Entry.php | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 819ff7f3e3c..292940ba53a 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -78,6 +78,7 @@ class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableVal protected $withEvents = true; protected $template; protected $layout; + protected $cachedCollectionInstance; public function __construct() { @@ -115,17 +116,20 @@ public function authors() public function collection($collection = null) { - return $this - ->fluentlyGetOrSet('collection') - ->setter(function ($collection) { - return $collection instanceof \Statamic\Contracts\Entries\Collection ? $collection->handle() : $collection; - }) - ->getter(function ($collection) { - return $collection ? Blink::once("collection-{$collection}", function () use ($collection) { - return Collection::findByHandle($collection); - }) : null; - }) - ->args(func_get_args()); + if (func_num_args() === 0) { + if ($this->cachedCollectionInstance) { + return $this->cachedCollectionInstance; + } + + return $this->cachedCollectionInstance = $this->collection ? Blink::once("collection-{$this->collection}", function () { + return Collection::findByHandle($this->collection); + }) : null; + } + + $this->cachedCollectionInstance = null; + $this->collection = $collection instanceof \Statamic\Contracts\Entries\Collection ? $collection->handle() : $collection; + + return $this; } public function blueprint($blueprint = null) From 7a0ed135c675b2169cdfb0b425ff14bb8bf3fd30 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 18:24:43 -0600 Subject: [PATCH 35/81] Refactors; cache computed callbacks While the Blink call for collection is unfortunate, its still an improvement over the repeated getter/setter calls in this method due to how many times it is called. Cache invalidation complexity here is not worth it at this time --- src/Entries/Entry.php | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 292940ba53a..51409d6dac1 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -78,7 +78,7 @@ class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableVal protected $withEvents = true; protected $template; protected $layout; - protected $cachedCollectionInstance; + protected $computedCallbackCache; public function __construct() { @@ -117,16 +117,12 @@ public function authors() public function collection($collection = null) { if (func_num_args() === 0) { - if ($this->cachedCollectionInstance) { - return $this->cachedCollectionInstance; - } - - return $this->cachedCollectionInstance = $this->collection ? Blink::once("collection-{$this->collection}", function () { + return $this->collection ? Blink::once("collection-{$this->collection}", function () { return Collection::findByHandle($this->collection); }) : null; } - $this->cachedCollectionInstance = null; + $this->computedCallbackCache = null; $this->collection = $collection instanceof \Statamic\Contracts\Entries\Collection ? $collection->handle() : $collection; return $this; @@ -1018,6 +1014,22 @@ public function getCpSearchResultBadge(): string protected function getComputedCallbacks() { - return Facades\Collection::getComputedCallbacks($this->collection); + if ($this->computedCallbackCache) { + return $this->computedCallbackCache; + } + + return $this->computedCallbackCache = Facades\Collection::getComputedCallbacks($this->collection); + } + + public function __serialize(): array + { + return Arr::except(get_object_vars($this), ['computedCallbackCache']); + } + + public function __unserialize(array $data): void + { + foreach ($data as $key => $value) { + $this->{$key} = $value; + } } } From a473076d51ea876c0d2045fa006120cd5691974f Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 20:18:19 -0600 Subject: [PATCH 36/81] Caches fieldtype calls Test adjustments due to cloned instance no longer tracking with mocked `$fieldtype =` --- src/Fields/Field.php | 5 ++++- src/Fields/Fieldtype.php | 7 +++++++ tests/Data/AugmentedTest.php | 22 ++++++++++++++++++---- tests/Data/HasAugmentedDataTest.php | 27 +++++++++++++++++++++------ tests/Fields/FieldTest.php | 6 +++--- 5 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/Fields/Field.php b/src/Fields/Field.php index 087be0d4e28..5a96303cce9 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Lang; use Rebing\GraphQL\Support\Field as GqlField; use Statamic\Contracts\Forms\Form; +use Statamic\Facades\Blink; use Statamic\Facades\GraphQL; use Statamic\Support\Arr; use Statamic\Support\Str; @@ -101,7 +102,9 @@ public function type() public function fieldtype() { - return FieldtypeRepository::find($this->type())->setField($this); + return (clone Blink::once('fieldtype'.$this->type(), function () { + return FieldtypeRepository::find($this->type()); + }))->setField($this); } public function display() diff --git a/src/Fields/Fieldtype.php b/src/Fields/Fieldtype.php index 9c1bca046a2..046dc1e1dc8 100644 --- a/src/Fields/Fieldtype.php +++ b/src/Fields/Fieldtype.php @@ -57,6 +57,13 @@ public function setField(Field $field) return $this; } + public function withoutField() + { + $this->field = null; + + return $this; + } + public function field(): ?Field { return $this->field; diff --git a/tests/Data/AugmentedTest.php b/tests/Data/AugmentedTest.php index fc5a709b193..f56ac1f284d 100644 --- a/tests/Data/AugmentedTest.php +++ b/tests/Data/AugmentedTest.php @@ -174,7 +174,7 @@ public function hello() $this->assertEquals('bar', $value->raw()); $this->assertEquals('AUGMENTED BAR', $value->value()); $this->assertEquals('foo', $value->handle()); - $this->assertEquals($fieldtype, $value->fieldtype()); + $this->assertEquals($fieldtype, $value->fieldtype()->withoutField()); $this->assertEquals($this->blueprintThing, $value->augmentable()); }); @@ -183,7 +183,7 @@ public function hello() $this->assertEquals('the-thing', $value->raw()); $this->assertEquals('AUGMENTED THE-THING', $value->value()); $this->assertEquals('slug', $value->handle()); - $this->assertEquals($fieldtype, $value->fieldtype()); + $this->assertEquals($fieldtype, $value->fieldtype()->withoutField()); $this->assertEquals($this->blueprintThing, $value->augmentable()); }); @@ -280,6 +280,9 @@ public function hello() }; $result = $augmented->all(); + $result['foo']->fieldtype()->withoutField(); + $result['slug']->fieldtype()->withoutField(); + $this->assertInstanceOf(AugmentedCollection::class, $result); $this->assertEquals([ 'foo' => $foo = new Value('bar', 'foo', $fieldtype, $this->blueprintThing), @@ -290,6 +293,8 @@ public function hello() ], $result->all()); $result = $augmented->select(['foo', 'hello']); + $result['foo']->fieldtype()->withoutField(); + $this->assertInstanceOf(AugmentedCollection::class, $result); $this->assertEveryItemIsInstanceOf(Value::class, $result); $this->assertEquals([ @@ -297,11 +302,16 @@ public function hello() 'hello' => $hello, ], $result->all()); + $result = $augmented->select('foo'); + $result['foo']->fieldtype()->withoutField(); + $this->assertEquals([ 'foo' => $foo, - ], $augmented->select('foo')->all()); + ], $result->all()); $result = $augmented->except(['slug', 'hello']); + $result['foo']->fieldtype()->withoutField(); + $this->assertInstanceOf(AugmentedCollection::class, $result); $this->assertEquals([ 'foo' => $foo, @@ -309,12 +319,16 @@ public function hello() 'supplemented' => $supplemented, ], $result->all()); + $result = $augmented->except('hello'); + $result['foo']->fieldtype()->withoutField(); + $result['slug']->fieldtype()->withoutField(); + $this->assertEquals([ 'foo' => $foo, 'slug' => $slug, 'the_slug' => $theSlug, 'supplemented' => $supplemented, - ], $augmented->except('hello')->all()); + ], $result->all()); } /** @test */ diff --git a/tests/Data/HasAugmentedDataTest.php b/tests/Data/HasAugmentedDataTest.php index 0e1e58c7d0a..f08211625ef 100644 --- a/tests/Data/HasAugmentedDataTest.php +++ b/tests/Data/HasAugmentedDataTest.php @@ -15,7 +15,7 @@ class HasAugmentedDataTest extends TestCase { /** @test */ - public function it_makes_an_augmented_instance() + public function aaa_it_makes_an_augmented_instance() { FieldtypeRepository::shouldReceive('find')->with('test')->andReturn($fieldtype = new class extends Fieldtype { @@ -56,7 +56,7 @@ public function blueprint() $this->assertEquals('FOO', $value->raw()); $this->assertEquals('foo', $value->handle()); $this->assertEquals($thing, $value->augmentable()); - $this->assertEquals($fieldtype, $value->fieldtype()); + $this->assertEquals($fieldtype, $value->fieldtype()->withoutField()); }); $this->assertEquals('BAR', $thing->augmentedValue('bar')); @@ -66,14 +66,29 @@ public function blueprint() 'foo' => new Value('FOO', 'foo', $fieldtype, $thing), 'bar' => 'BAR', ]; - $this->assertEquals($expectedArr, $thing->augmented()->all()->all()); - $this->assertEquals($expectedArr, $thing->toAugmentedArray()); + + $result = $thing->augmented()->all(); + $result['foo']->fieldtype()->withoutField(); + + $this->assertEquals($expectedArr, $result->all()); + $result = $thing->toAugmentedArray(); + $result['foo']->fieldtype()->withoutField(); + + $this->assertEquals($expectedArr, $result); $expectedSelectArr = [ 'foo' => new Value('FOO', 'foo', $fieldtype, $thing), 'bar' => 'BAR', ]; - $this->assertEquals($expectedSelectArr, $thing->augmented()->select(['foo', 'bar'])->all()); - $this->assertEquals($expectedSelectArr, $thing->toAugmentedArray(['foo', 'bar'])); + + $result = $thing->augmented()->select(['foo', 'bar']); + $result['foo']->fieldtype()->withoutField(); + + $this->assertEquals($expectedSelectArr, $result->all()); + + $result = $thing->toAugmentedArray(['foo', 'bar']); + $result['foo']->fieldtype()->withoutField(); + + $this->assertEquals($expectedSelectArr, $result); } } diff --git a/tests/Fields/FieldTest.php b/tests/Fields/FieldTest.php index 55e1e66b0e3..0cbc9e0a238 100644 --- a/tests/Fields/FieldTest.php +++ b/tests/Fields/FieldTest.php @@ -62,7 +62,7 @@ public function it_gets_the_fieldtype() $field = new Field('test', ['type' => 'the_fieldtype']); - $this->assertEquals($fieldtype, $field->fieldtype()); + $this->assertEquals($fieldtype, $field->fieldtype()->withoutField()); } /** @test */ @@ -517,7 +517,7 @@ public function shallowAugment($data) $this->assertNotSame($field, $augmented); $value = $augmented->value(); $this->assertInstanceOf(Value::class, $value); - $this->assertEquals($fieldtype, $value->fieldtype()); + $this->assertEquals($fieldtype, $value->fieldtype()->withoutField()); $this->assertEquals('test', $value->handle()); $this->assertEquals('foo', $value->raw()); $this->assertEquals('foo augmented', $value->value()); @@ -527,7 +527,7 @@ public function shallowAugment($data) $this->assertNotSame($field, $augmented); $value = $augmented->value(); $this->assertInstanceOf(Value::class, $value); - $this->assertEquals($fieldtype, $value->fieldtype()); + $this->assertEquals($fieldtype, $value->fieldtype()->withoutField()); $this->assertEquals('test', $value->handle()); $this->assertEquals('foo', $value->raw()); $this->assertEquals('foo shallow augmented', $value->value()); From f5eee7c12a6b3404486bf5cfef09c04ed7997bff Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 2 Mar 2024 20:32:10 -0600 Subject: [PATCH 37/81] Ensure fieldtype materializes values --- src/Data/Concerns/ResolvesValues.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Data/Concerns/ResolvesValues.php b/src/Data/Concerns/ResolvesValues.php index b54246bd621..7fead66c110 100644 --- a/src/Data/Concerns/ResolvesValues.php +++ b/src/Data/Concerns/ResolvesValues.php @@ -47,4 +47,11 @@ public function isRelationship(): bool return parent::isRelationship(); } + + public function fieldtype() + { + $this->resolve(); + + return parent::fieldtype(); + } } From 167f35326358afc05baf8e8cd7fc8a9ba65fb503 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 09:04:18 -0600 Subject: [PATCH 38/81] Cache external url/uri results --- src/Facades/Endpoint/URL.php | 23 +++++++++++++++++++++-- src/View/State/ClearState.php | 3 +++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Facades/Endpoint/URL.php b/src/Facades/Endpoint/URL.php index cd4e1261b65..7df009e5f84 100644 --- a/src/Facades/Endpoint/URL.php +++ b/src/Facades/Endpoint/URL.php @@ -14,6 +14,8 @@ */ class URL { + protected static $externalUriCache = []; + /** * Removes occurrences of "//" in a $path (except when part of a protocol) * Alias of Path::tidy(). @@ -221,14 +223,31 @@ public function format($url) */ public function isExternal($url) { - if (! $url || Str::startsWith($url, ['/', '#'])) { + if (isset(self::$externalUriCache[$url])) { + return self::$externalUriCache[$url]; + } + + if (! $url) { return false; } - return ! Pattern::startsWith( + if (Str::startsWith($url, ['/', '#'])) { + return self::$externalUriCache[$url] = false; + } + + $isExternal = ! Pattern::startsWith( Str::ensureRight($url, '/'), Site::current()->absoluteUrl() ); + + self::$externalUriCache[$url] = $isExternal; + + return $isExternal; + } + + public function clearExternalUrlCache() + { + self::$externalUriCache = []; } /** diff --git a/src/View/State/ClearState.php b/src/View/State/ClearState.php index 8ca8ef86ca9..6be64601671 100644 --- a/src/View/State/ClearState.php +++ b/src/View/State/ClearState.php @@ -2,10 +2,13 @@ namespace Statamic\View\State; +use Statamic\Facades\URL; + class ClearState { public function handle() { StateManager::resetState(); + URL::clearExternalUrlCache(); } } From 4ca6eb1ffd075cb40977d7475de0228486b841ee Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 09:06:41 -0600 Subject: [PATCH 39/81] Cache site absolute url --- src/Sites/Site.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Sites/Site.php b/src/Sites/Site.php index 646d1481882..655d35c9d36 100644 --- a/src/Sites/Site.php +++ b/src/Sites/Site.php @@ -12,6 +12,7 @@ class Site implements Augmentable protected $handle; protected $config; + protected $absoluteUrlCache; public function __construct($handle, $config) { @@ -67,11 +68,15 @@ public function attributes() public function absoluteUrl() { + if ($this->absoluteUrlCache !== null) { + return $this->absoluteUrlCache; + } + if (Str::startsWith($url = $this->url(), '/')) { $url = Str::ensureLeft($url, request()->getSchemeAndHttpHost()); } - return Str::removeRight($url, '/'); + return $this->absoluteUrlCache = Str::removeRight($url, '/'); } public function relativePath($url) From dd4f5c359cf34d79890d9b59b1c17b37ad8fbb5b Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 09:28:42 -0600 Subject: [PATCH 40/81] Cache a collection tree's structure on the instance --- src/Structures/CollectionTree.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Structures/CollectionTree.php b/src/Structures/CollectionTree.php index 328a93b4a24..0f754fa88f2 100644 --- a/src/Structures/CollectionTree.php +++ b/src/Structures/CollectionTree.php @@ -14,9 +14,15 @@ class CollectionTree extends Tree implements TreeContract { + protected $structureCache; + public function structure() { - return Blink::once('collection-tree-structure-'.$this->handle(), function () { + if ($this->structureCache) { + return $this->structureCache; + } + + return $this->structureCache = Blink::once('collection-tree-structure-'.$this->handle(), function () { return Collection::findByHandle($this->handle())->structure(); }); } From f565d42902de565d4bb78b16be3c13bdaa63c6af Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 09:35:43 -0600 Subject: [PATCH 41/81] Cache blueprint instance on nav structure --- src/Structures/Nav.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Structures/Nav.php b/src/Structures/Nav.php index 1ee7539f75b..c040b64b4ea 100644 --- a/src/Structures/Nav.php +++ b/src/Structures/Nav.php @@ -22,6 +22,7 @@ class Nav extends Structure implements Contract use ExistsAsFile; protected $collections; + protected $blueprintCache; public function save() { @@ -121,8 +122,12 @@ public function existsIn($site) public function blueprint() { + if ($this->blueprintCache) { + return $this->blueprintCache; + } + if (Blink::has($blink = 'nav-blueprint-'.$this->handle())) { - return Blink::get($blink); + return $this->blueprintCache = Blink::get($blink); } $blueprint = Blueprint::find('navigation.'.$this->handle()) @@ -132,6 +137,6 @@ public function blueprint() NavBlueprintFound::dispatch($blueprint, $this); - return $blueprint; + return $this->blueprintCache = $blueprint; } } From 82b15a75f27d508f2853cbcaebc8da2ca990aa8f Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 09:36:50 -0600 Subject: [PATCH 42/81] Cache structure on nav tree instance --- src/Structures/NavTree.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Structures/NavTree.php b/src/Structures/NavTree.php index b46145e75e0..b908d16da14 100644 --- a/src/Structures/NavTree.php +++ b/src/Structures/NavTree.php @@ -14,9 +14,15 @@ class NavTree extends Tree implements TreeContract { + protected $structureCache; + public function structure() { - return Blink::once('nav-tree-structure-'.$this->handle(), function () { + if ($this->structureCache) { + return $this->structureCache; + } + + return $this->structureCache = Blink::once('nav-tree-structure-'.$this->handle(), function () { return Nav::findByHandle($this->handle()); }); } From f98b7235508d590a0eb78c05ace512eea6059b00 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 09:52:26 -0600 Subject: [PATCH 43/81] Remove getter/setter from dated() as its called very frequently --- src/Entries/Collection.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Entries/Collection.php b/src/Entries/Collection.php index a96358a0748..af1de9c499e 100644 --- a/src/Entries/Collection.php +++ b/src/Entries/Collection.php @@ -135,7 +135,13 @@ public function autoGeneratesTitles() public function dated($dated = null) { - return $this->fluentlyGetOrSet('dated')->args(func_get_args()); + if (func_num_args() === 0) { + return $this->dated; + } + + $this->dated = $dated; + + return $this; } public function orderable() From 3340b73b4ce6c712febe6305c458ae772e31c071 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 10:01:36 -0600 Subject: [PATCH 44/81] =?UTF-8?q?=F0=9F=A7=B9=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/View/State/ClearState.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/State/ClearState.php b/src/View/State/ClearState.php index 2bbed0caf1d..da19b359ede 100644 --- a/src/View/State/ClearState.php +++ b/src/View/State/ClearState.php @@ -2,8 +2,8 @@ namespace Statamic\View\State; -use Statamic\Statamic; use Statamic\Facades\URL; +use Statamic\Statamic; class ClearState { From eacdfb82623a0bf63537f234ca1ed08d6b73325f Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 10:09:20 -0600 Subject: [PATCH 45/81] Add instance cache for site() method --- src/Entries/Entry.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 819ff7f3e3c..efe35e2854c 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -78,6 +78,7 @@ class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableVal protected $withEvents = true; protected $template; protected $layout; + protected $siteCache; public function __construct() { @@ -95,6 +96,8 @@ public function locale($locale = null) return $this ->fluentlyGetOrSet('locale') ->setter(function ($locale) { + $this->siteCache = null; + return $locale instanceof \Statamic\Sites\Site ? $locale->handle() : $locale; }) ->getter(function ($locale) { @@ -105,7 +108,11 @@ public function locale($locale = null) public function site() { - return Site::get($this->locale()); + if ($this->siteCache) { + return $this->siteCache; + } + + return $this->siteCache = Site::get($this->locale()); } public function authors() From 5a22464935389a2d8201e980094d1e3c18e9a108 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 10:30:29 -0600 Subject: [PATCH 46/81] Removes fluent getter/setter from entry's collection() --- src/Entries/Entry.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 819ff7f3e3c..a4896d19665 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -115,17 +115,15 @@ public function authors() public function collection($collection = null) { - return $this - ->fluentlyGetOrSet('collection') - ->setter(function ($collection) { - return $collection instanceof \Statamic\Contracts\Entries\Collection ? $collection->handle() : $collection; - }) - ->getter(function ($collection) { - return $collection ? Blink::once("collection-{$collection}", function () use ($collection) { - return Collection::findByHandle($collection); - }) : null; - }) - ->args(func_get_args()); + if (func_num_args() === 0) { + return $this->collection ? Blink::once("collection-{$this->collection}", function () { + return Collection::findByHandle($this->collection); + }) : null; + } + + $this->collection = $collection instanceof \Statamic\Contracts\Entries\Collection ? $collection->handle() : $collection; + + return $this; } public function blueprint($blueprint = null) From d38ee8f86a6813d616088d0b01c994036e7e963b Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 10:37:16 -0600 Subject: [PATCH 47/81] Cache entry date/time related properties --- src/Entries/Entry.php | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 819ff7f3e3c..27027deafe5 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -78,6 +78,9 @@ class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableVal protected $withEvents = true; protected $template; protected $layout; + protected $hasDate; + protected $hasTime; + protected $hasSeconds; public function __construct() { @@ -118,6 +121,8 @@ public function collection($collection = null) return $this ->fluentlyGetOrSet('collection') ->setter(function ($collection) { + $this->clearDateTimePropertyCaches(); + return $collection instanceof \Statamic\Contracts\Entries\Collection ? $collection->handle() : $collection; }) ->getter(function ($collection) { @@ -158,6 +163,8 @@ public function blueprint($blueprint = null) return $blueprint; }) ->setter(function ($blueprint) use ($key) { + $this->clearDateTimePropertyCaches(); + Blink::forget($key); return $blueprint instanceof \Statamic\Fields\Blueprint ? $blueprint->handle() : $blueprint; @@ -539,27 +546,46 @@ public function date($date = null) ->args(func_get_args()); } + protected function clearDateTimePropertyCaches() + { + $this->hasDate = null; + $this->hasTime = null; + $this->hasSeconds = null; + } + public function hasDate() { - return $this->collection()->dated(); + if ($this->hasDate !== null) { + return $this->hasDate; + } + + return $this->hasDate = $this->collection()->dated(); } public function hasTime() { + if ($this->hasTime !== null) { + return $this->hasTime; + } + if (! $this->hasDate()) { - return false; + return $this->hasTime = false; } - return $this->blueprint()->field('date')->fieldtype()->timeEnabled(); + return $this->hasTime = $this->blueprint()->field('date')->fieldtype()->timeEnabled(); } public function hasSeconds() { + if ($this->hasSeconds !== null) { + return $this->hasSeconds; + } + if (! $this->hasTime()) { - return false; + return $this->hasSeconds = false; } - return $this->blueprint()->field('date')->fieldtype()->secondsEnabled(); + return $this->hasSeconds = $this->blueprint()->field('date')->fieldtype()->secondsEnabled(); } public function sites() From 1c63e7acda74d0010484b17515610c5528ecaef7 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 11:13:23 -0600 Subject: [PATCH 48/81] Cache urls on Page instances --- src/Structures/Page.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 7455bd64d38..3a73dd67bbf 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -39,6 +39,8 @@ class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializab protected $title; protected $depth; protected $data = []; + protected $absoluteUrl; + protected $absoluteUrlWithoutRedirect; public function __construct() { @@ -210,20 +212,28 @@ public function uri() public function absoluteUrl() { + if ($this->absoluteUrl !== null) { + return $this->absoluteUrl; + } + if ($this->url) { - return URL::makeAbsolute($this->url); + return $this->absoluteUrl = URL::makeAbsolute($this->url); } - return optional($this->entry())->absoluteUrl(); + return $this->absoluteUrl = optional($this->entry())->absoluteUrl(); } public function absoluteUrlWithoutRedirect() { + if ($this->absoluteUrlWithoutRedirect !== null) { + return $this->absoluteUrlWithoutRedirect; + } + if ($this->url) { - return $this->absoluteUrl(); + return $this->absoluteUrlWithoutRedirect = $this->absoluteUrl(); } - return optional($this->entry())->absoluteUrlWithoutRedirect(); + return $this->absoluteUrlWithoutRedirect = optional($this->entry())->absoluteUrlWithoutRedirect(); } public function isRoot() From effac3b2d6dd241596d2ef51d4f73c05ae07a8d0 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 11:17:16 -0600 Subject: [PATCH 49/81] Cache structure call on page instances --- src/Structures/Page.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 7455bd64d38..8c3da6ceefb 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -39,6 +39,7 @@ class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializab protected $title; protected $depth; protected $data = []; + protected $structure; public function __construct() { @@ -393,7 +394,11 @@ public function toResponse($request) public function structure() { - return $this->tree->structure(); + if ($this->structure !== null) { + return $this->structure; + } + + return $this->structure = $this->tree->structure(); } public function routeData() From 65bf0daced8cbb3e6964bc2888d2f3d8bca98095 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 11:18:49 -0600 Subject: [PATCH 50/81] Cache route data on page instances --- src/Structures/Page.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 7455bd64d38..1b8063a9270 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -39,6 +39,7 @@ class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializab protected $title; protected $depth; protected $data = []; + protected $routeData; public function __construct() { @@ -398,7 +399,11 @@ public function structure() public function routeData() { - return $this->entry()->routeData(); + if ($this->routeData !== null) { + return $this->routeData; + } + + return $this->routeData = $this->entry()->routeData(); } public function published() From fecfc467fae64e5b0c33b2ca32d0d7711f7c5fe3 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 11:20:50 -0600 Subject: [PATCH 51/81] Cache blueprint calls on Page instances --- src/Structures/Page.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 7455bd64d38..d61c33ec40b 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -39,6 +39,7 @@ class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializab protected $title; protected $depth; protected $data = []; + protected $blueprint; public function __construct() { @@ -418,8 +419,12 @@ public function status() public function blueprint() { + if ($this->blueprint !== null) { + return $this->blueprint; + } + if ($this->structure() instanceof Nav) { - return $this->structure()->blueprint(); + return $this->blueprint = $this->structure()->blueprint(); } } From 9457ba8b7d52274265fe1ddba837aa01b90936fa Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 11:21:34 -0600 Subject: [PATCH 52/81] Cache status method on Page instances --- src/Structures/Page.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 7455bd64d38..96170492f0b 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -39,6 +39,7 @@ class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializab protected $title; protected $depth; protected $data = []; + protected $status; public function __construct() { @@ -413,7 +414,11 @@ public function private() public function status() { - return optional($this->entry())->status(); + if ($this->status !== null) { + return $this->status; + } + + return $this->status = optional($this->entry())->status(); } public function blueprint() From a50301b7b4f1014b7f4cb2d477844bc9f925c56b Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 11:28:49 -0600 Subject: [PATCH 53/81] Cache the results of the entry method on Page instances --- src/Structures/Page.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 7455bd64d38..bea68aea0c5 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -39,6 +39,7 @@ class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializab protected $title; protected $depth; protected $data = []; + protected $entry; public function __construct() { @@ -124,15 +125,19 @@ public function setEntry($reference): self public function entry(): ?Entry { + if ($this->entry !== null) { + return $this->entry; + } + if (! $this->reference) { return null; } if ($cached = Blink::store('structure-entries')->get($this->reference)) { - return $cached; + return $this->entry = $cached; } - return $this->tree->entry($this->reference); + return $this->entry = $this->tree->entry($this->reference); } public function reference() From f63fe8781d1e041b7cdc1d586a9e86085ca9e439 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 3 Mar 2024 12:51:26 -0600 Subject: [PATCH 54/81] Refactor Blink to avoid wildcard calls under the hood --- src/Support/Blink.php | 4 +--- src/Support/BlinkWrapper.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 src/Support/BlinkWrapper.php diff --git a/src/Support/Blink.php b/src/Support/Blink.php index aa0cb8219f4..9458771fbdd 100644 --- a/src/Support/Blink.php +++ b/src/Support/Blink.php @@ -2,15 +2,13 @@ namespace Statamic\Support; -use Spatie\Blink\Blink as SpatieBlink; - class Blink { protected $stores = []; public function store($name = 'default') { - return $this->stores[$name] = $this->stores[$name] ?? new SpatieBlink; + return $this->stores[$name] = $this->stores[$name] ?? new BlinkWrapper(); } public function __call($method, $args) diff --git a/src/Support/BlinkWrapper.php b/src/Support/BlinkWrapper.php new file mode 100644 index 00000000000..e5b32eccc16 --- /dev/null +++ b/src/Support/BlinkWrapper.php @@ -0,0 +1,18 @@ +values); + } + + public function get(string $key, $default = null) + { + return array_key_exists($key, $this->values) ? $this->values[$key] : $default; + } +} From 63ce38c285a8e01e15e23a981bcf165b57260416 Mon Sep 17 00:00:00 2001 From: John Koster Date: Wed, 6 Mar 2024 18:59:09 -0600 Subject: [PATCH 55/81] Cache mounted collections --- src/Entries/Collection.php | 3 +++ src/Stache/Repositories/CollectionRepository.php | 13 ++++++++++--- tests/Data/Entries/CollectionTest.php | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Entries/Collection.php b/src/Entries/Collection.php index 2e287cb41c9..40e2dee3484 100644 --- a/src/Entries/Collection.php +++ b/src/Entries/Collection.php @@ -450,6 +450,7 @@ public function save() Facades\Collection::save($this); Blink::forget('collection-handles'); + Blink::forget('mounted-collections'); Blink::flushStartingWith("collection-{$this->id()}"); if ($isNew) { @@ -735,6 +736,8 @@ public function delete() CollectionDeleted::dispatch($this); + Blink::forget('mounted-collections'); + return true; } diff --git a/src/Stache/Repositories/CollectionRepository.php b/src/Stache/Repositories/CollectionRepository.php index 4412d890537..81c07f8a293 100644 --- a/src/Stache/Repositories/CollectionRepository.php +++ b/src/Stache/Repositories/CollectionRepository.php @@ -35,6 +35,15 @@ public function find($id): ?Collection return $this->findByHandle($id); } + protected function mountedCollections(): IlluminateCollection + { + return Blink::once('mounted-collections', function () { + return $this->all()->keyBy(function ($collection) { + return $collection->mount()?->id(); + })->filter(); + }); + } + public function findByHandle($handle): ?Collection { return $this->store->getItem($handle); @@ -46,9 +55,7 @@ public function findByMount($mount): ?Collection return null; } - return $this->all()->first(function ($collection) use ($mount) { - return optional($collection->mount())->id() === $mount->id(); - }); + return $this->mountedCollections()->get($mount->id()); } public function make(?string $handle = null): Collection diff --git a/tests/Data/Entries/CollectionTest.php b/tests/Data/Entries/CollectionTest.php index e5e79b1966c..30fa179bfc3 100644 --- a/tests/Data/Entries/CollectionTest.php +++ b/tests/Data/Entries/CollectionTest.php @@ -477,6 +477,7 @@ public function it_saves_the_collection_through_the_api() Facades\Collection::shouldReceive('save')->with($collection)->once(); Facades\Collection::shouldReceive('handleExists')->with('test')->once(); Facades\Blink::shouldReceive('forget')->with('collection-handles')->once(); + Facades\Blink::shouldReceive('forget')->with('mounted-collections')->once(); Facades\Blink::shouldReceive('flushStartingWith')->with('collection-test')->once(); $return = $collection->save(); From 7778875cae394d56ad131ec322b5797b43444b3b Mon Sep 17 00:00:00 2001 From: John Koster Date: Wed, 6 Mar 2024 19:02:01 -0600 Subject: [PATCH 56/81] Reuse referenceExists return value --- src/Structures/TreeBuilder.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Structures/TreeBuilder.php b/src/Structures/TreeBuilder.php index a99f73bf917..4c22aa7e340 100644 --- a/src/Structures/TreeBuilder.php +++ b/src/Structures/TreeBuilder.php @@ -84,18 +84,19 @@ protected function transformTreeForController($tree) return collect($tree)->map(function ($item) { $page = $item['page']; $collection = $page->collection(); + $referenceExists = $page->referenceExists(); return [ 'id' => $page->id(), 'entry' => $page->reference(), 'title' => $page->hasCustomTitle() ? $page->title() : null, - 'entry_title' => $page->referenceExists() ? $page->entry()->value('title') : null, - 'url' => $page->referenceExists() ? $page->url() : null, + 'entry_title' => $referenceExists ? $page->entry()->value('title') : null, + 'url' => $referenceExists ? $page->url() : null, 'edit_url' => $page->editUrl(), - 'can_delete' => $page->referenceExists() ? User::current()->can('delete', $page->entry()) : true, + 'can_delete' => $referenceExists ? User::current()->can('delete', $page->entry()) : true, 'slug' => $page->slug(), - 'status' => $page->referenceExists() ? $page->status() : null, - 'redirect' => $page->referenceExists() ? $page->entry()->get('redirect') : null, + 'status' => $referenceExists ? $page->status() : null, + 'redirect' => $referenceExists ? $page->entry()->get('redirect') : null, 'collection' => ! $collection ? null : [ 'handle' => $collection->handle(), 'title' => $collection->title(), From e5ce6391e9e7807d038614d28a884f75dd4f427b Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 8 Mar 2024 10:18:00 -0600 Subject: [PATCH 57/81] Implements `pluck` on query builder This implementation will resolve values from the Stache indexes --- src/Stache/Query/Builder.php | 34 +++++++++++-- src/Stache/Stores/AggregateStore.php | 9 ++++ src/Stache/Stores/Store.php | 51 ++++++++++++++++++++ tests/Data/Entries/EntryQueryBuilderTest.php | 31 ++++++++++++ 4 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/Stache/Query/Builder.php b/src/Stache/Query/Builder.php index 139d5553d7f..5381cd09cf0 100644 --- a/src/Stache/Query/Builder.php +++ b/src/Stache/Query/Builder.php @@ -2,6 +2,7 @@ namespace Statamic\Stache\Query; +use Illuminate\Support\Str; use Statamic\Data\DataCollection; use Statamic\Query\Builder as BaseBuilder; use Statamic\Stache\Stores\Store; @@ -21,15 +22,42 @@ public function count() return $this->getFilteredAndLimitedKeys()->count(); } - public function get($columns = ['*']) + protected function resolveKeys() { $keys = $this->getFilteredKeys(); $keys = $this->orderKeys($keys); - $keys = $this->limitKeys($keys); + return $this->limitKeys($keys); + } + + public function pluck($column, $key = null) + { + $keys = $this->resolveKeys(); + + return $this->store->getFromIndex( + $this->getKeysForIndexQuery($keys), + $column, + $key + ); + } + + protected function getKeysForIndexQuery($keys) + { + return $keys->map(function ($key) { + $queryKey = Str::after($key, '::'); + + if (! Str::contains($queryKey, '-') && is_numeric($queryKey)) { + return intval($queryKey); + } + + return $queryKey; + }); + } - $items = $this->getItems($keys); + public function get($columns = ['*']) + { + $items = $this->getItems($this->resolveKeys()); $items->each(fn ($item) => $item ->selectedQueryColumns($this->columns ?? $columns) diff --git a/src/Stache/Stores/AggregateStore.php b/src/Stache/Stores/AggregateStore.php index a531aaeca6b..8c410d660a7 100644 --- a/src/Stache/Stores/AggregateStore.php +++ b/src/Stache/Stores/AggregateStore.php @@ -14,6 +14,15 @@ public function __construct() $this->stores = collect(); } + protected function resolveFromIndex($keys, $column) + { + return $this->stores()->mapWithKeys(function ($store) use ($column) { + return $store->resolveIndex($column)->load()->items(); + })->where(function ($value, $key) use (&$keys) { + return $keys->has($key); + }); + } + public function store($key) { if (! $this->stores->has($key)) { diff --git a/src/Stache/Stores/Store.php b/src/Stache/Stores/Store.php index 41dc66bc9f5..37346e91706 100644 --- a/src/Stache/Stores/Store.php +++ b/src/Stache/Stores/Store.php @@ -27,6 +27,57 @@ abstract class Store protected $shouldCacheFileItems = false; protected $modified; protected $keys; + protected $identifiedBy = 'id'; + + protected function resolveFromIndex($keys, $column) + { + return $this->resolveIndex($column) + ->load() + ->items() + ->where(function ($value, $key) use (&$keys) { + return $keys->has($key); + }); + } + + private function isValidKey($value) + { + if (is_string($value) || is_int($value)) { + return true; + } + + return false; + } + + public function getFromIndex($keys, $column, $key = null) + { + if ($column === $this->identifiedBy && $key === null) { + return $keys; + } + + $keys = $keys->flip(); + $values = $this->resolveFromIndex($keys, $column); + + if ($key === null) { + return $values->values(); + } + + $keyValues = $this->resolveFromIndex($keys, $key); + $newValues = []; + + foreach ($keys->keys() as $keyValue) { + $newKeyValue = $keyValues[$keyValue] ?? null; + + if (! $this->isValidKey($newKeyValue)) { + continue; + } + + $newValue = $values[$keyValue] ?? null; + + $newValues[$newKeyValue] = $newValue; + } + + return collect($newValues); + } public function directory($directory = null) { diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index ac60eedbc20..43753b0ee27 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -4,6 +4,7 @@ use Facades\Tests\Factories\EntryFactory; use Illuminate\Support\Carbon; +use Illuminate\Support\Str; use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; @@ -769,4 +770,34 @@ public function entries_are_found_using_lazy() $this->assertInstanceOf(\Illuminate\Support\LazyCollection::class, $entries); $this->assertCount(3, $entries); } + + /** @test */ + public function pluck_can_be_used_to_retrieve_values_from_index() + { + $this->createDummyCollectionAndEntries(); + + $this->assertEquals(collect([1, 2, 3]), Entry::query()->pluck('id')); + + $paths = Entry::query()->pluck('path')->map(fn ($path) => Str::afterLast($path, '/')); + + $this->assertEquals(collect([ + 'post-1.md', + 'post-2.md', + 'post-3.md', + ]), $paths); + + $this->assertEquals(collect([ + 1 => 'post-1', + 2 => 'post-2', + 3 => 'post-3', + ]), Entry::query()->pluck('slug', 'id')); + + $this->assertEquals(collect([ + 3 => 'post-3', + ]), Entry::query()->where('id', 3)->pluck('slug', 'id')); + + $this->assertEquals(collect([ + 'post-3' => 3, + ]), Entry::query()->where('id', 3)->pluck('id', 'slug')); + } } From 394b73af90801e6ee3a8559affcedc57b12b9359 Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 8 Mar 2024 10:18:34 -0600 Subject: [PATCH 58/81] Refactor collection structure to use pluck when validating trees --- src/Structures/CollectionStructure.php | 3 +- .../Structures/CollectionStructureTest.php | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Structures/CollectionStructure.php b/src/Structures/CollectionStructure.php index e2716b03358..3edb98f43a6 100644 --- a/src/Structures/CollectionStructure.php +++ b/src/Structures/CollectionStructure.php @@ -74,8 +74,7 @@ public function validateTree(array $tree, string $locale): array $thisCollectionsEntries = $this->collection()->queryEntries() ->where('site', $locale) - ->get(['id', 'site']) - ->map->id(); + ->pluck('id'); $otherCollectionEntries = $entryIds->diff($thisCollectionsEntries); diff --git a/tests/Data/Structures/CollectionStructureTest.php b/tests/Data/Structures/CollectionStructureTest.php index 4cd3f322ec5..198115dd30b 100644 --- a/tests/Data/Structures/CollectionStructureTest.php +++ b/tests/Data/Structures/CollectionStructureTest.php @@ -19,6 +19,7 @@ class CollectionStructureTest extends StructureTestCase private $collection; private $entryQueryBuilder; private $queryBuilderGetReturnValue; + private $queryBuilderPluckReturnValue; public function setUp(): void { @@ -30,6 +31,9 @@ public function setUp(): void $this->entryQueryBuilder->shouldReceive('get')->andReturnUsing(function () { return $this->queryBuilderGetReturnValue(); }); + $this->entryQueryBuilder->shouldReceive('pluck')->andReturnUsing(function () { + return $this->queryBuilderPluckReturnValue(); + }); $this->collection = $this->mock(Collection::class); $this->collection->shouldReceive('queryEntries')->andReturn($this->entryQueryBuilder); @@ -47,6 +51,11 @@ public function queryBuilderGetReturnValue() return $this->queryBuilderGetReturnValue ?? collect(); } + public function queryBuilderPluckReturnValue() + { + return $this->queryBuilderPluckReturnValue ?? collect(); + } + /** @test */ public function it_gets_and_sets_the_handle() { @@ -84,6 +93,10 @@ public function it_makes_a_tree() Entry::make()->id('1'), ]); + $this->queryBuilderPluckReturnValue = collect([ + 1, + ]); + $tree = $structure->makeTree('fr', [ ['entry' => 1], ]); @@ -261,6 +274,11 @@ public function the_tree_root_can_have_children_when_not_expecting_root() Entry::make()->id('456'), ]); + $this->queryBuilderPluckReturnValue = collect([ + 123, + 456, + ]); + parent::the_tree_root_can_have_children_when_not_expecting_root(); } @@ -274,6 +292,11 @@ public function only_entries_belonging_to_the_associated_collection_may_be_in_th Entry::make()->id('2'), ]); + $this->queryBuilderPluckReturnValue = collect([ + 1, + 2, + ]); + $validated = $this->structure('test')->validateTree([ [ 'entry' => '1', @@ -308,6 +331,14 @@ public function entries_not_explicitly_in_the_tree_should_be_appended_to_the_end Entry::make()->id('5'), ]); + $this->queryBuilderPluckReturnValue = collect([ + 1, + 2, + 3, + 4, + 5, + ]); + $actual = $this->structure('test')->validateTree([ [ 'entry' => '1', From 27425099009bedb04a265d990a4d63683d3baca0 Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 8 Mar 2024 14:13:48 -0600 Subject: [PATCH 59/81] Ensure the index's values have been loaded --- src/Stache/Stores/BasicStore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stache/Stores/BasicStore.php b/src/Stache/Stores/BasicStore.php index 63b28b0b2bf..d0a5af8e54f 100644 --- a/src/Stache/Stores/BasicStore.php +++ b/src/Stache/Stores/BasicStore.php @@ -47,7 +47,7 @@ protected function getCachedItem($key) foreach ($item->receivesIndexValues() as $index) { Stache::itemUsingIndexValues($index, $item); - $value = $this->resolveIndex($index)->get($id); + $value = $this->resolveIndex($index)->load()->get($id); if ($value) { $item->withIndexedValue($index, $value); From aa5a9c14f247d4676f87fa62964f9807d7337fc3 Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 8 Mar 2024 17:34:50 -0600 Subject: [PATCH 60/81] Revert for now. --- src/Stache/Stores/BasicStore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stache/Stores/BasicStore.php b/src/Stache/Stores/BasicStore.php index ca1c2baa291..61b19a5725b 100644 --- a/src/Stache/Stores/BasicStore.php +++ b/src/Stache/Stores/BasicStore.php @@ -55,7 +55,7 @@ protected function getCachedItem($key) foreach ($item->receivesIndexValues() as $index) { Stache::itemUsingIndexValues($index, $item); - $value = $this->resolveIndex($index)->load()->get($id); + $value = $this->resolveIndex($index)->get($id); if ($value) { $item->withIndexedValue($index, $value); From 6dc2217c1fd9842f10f43639913b1474bcd60f30 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 9 Mar 2024 11:36:28 -0600 Subject: [PATCH 61/81] Orders Staches indexes; uses index order to optimize some sorts --- src/Sites/Site.php | 2 +- src/Stache/Indexes/Index.php | 13 ++++ src/Stache/Query/Builder.php | 59 +++++++++++++++++++ src/Stache/Query/EntryQueryBuilder.php | 8 +++ src/Stache/Query/TermQueryBuilder.php | 8 +++ tests/Auth/UserGroupTest.php | 2 +- tests/Data/Entries/EntryQueryBuilderTest.php | 8 +-- .../Data/Taxonomies/TermQueryBuilderTest.php | 6 +- tests/Data/Users/UserQueryBuilderTest.php | 16 ++--- tests/Feature/GraphQL/TermsTest.php | 6 +- tests/Feature/GraphQL/UsersTest.php | 2 +- tests/Feature/Taxonomies/TermEntriesTest.php | 18 +++--- 12 files changed, 119 insertions(+), 29 deletions(-) diff --git a/src/Sites/Site.php b/src/Sites/Site.php index 646d1481882..1c54830292a 100644 --- a/src/Sites/Site.php +++ b/src/Sites/Site.php @@ -31,7 +31,7 @@ public function name() public function locale() { - return $this->config['locale']; + return $this->config['locale'] ?? null; } public function shortLocale() diff --git a/src/Stache/Indexes/Index.php b/src/Stache/Indexes/Index.php index c147917cd06..aab37a3fb8f 100644 --- a/src/Stache/Indexes/Index.php +++ b/src/Stache/Indexes/Index.php @@ -3,6 +3,7 @@ namespace Statamic\Stache\Indexes; use Illuminate\Support\Facades\Cache; +use Statamic\Facades\Compare; use Statamic\Facades\Stache; use Statamic\Statamic; @@ -104,8 +105,20 @@ public function isCached() return Cache::has($this->cacheKey()); } + protected function orderIndex() + { + // Order all indexes in ascending order. If we want + // a descending sort later, we can reverse the + // keys after doing our index comparisons. + uasort($this->items, function ($a, $b) { + return Compare::values($a, $b); + }); + } + public function cache() { + $this->orderIndex(); + Cache::forever($this->cacheKey(), $this->items); } diff --git a/src/Stache/Query/Builder.php b/src/Stache/Query/Builder.php index 139d5553d7f..67b5c707999 100644 --- a/src/Stache/Query/Builder.php +++ b/src/Stache/Query/Builder.php @@ -4,6 +4,7 @@ use Statamic\Data\DataCollection; use Statamic\Query\Builder as BaseBuilder; +use Statamic\Stache\Stores\AggregateStore; use Statamic\Stache\Stores\Store; abstract class Builder extends BaseBuilder @@ -57,6 +58,60 @@ public function inRandomOrder() return $this; } + protected function prepareKeysForOptimizedSort($keys) + { + return $keys->combine($keys); + } + + protected function getOptimizedSortIndex() + { + if (count($this->orderBys) != 1) { + return null; + } + + $indexName = $this->orderBys[0]->sort; + + $store = $this->store; + + if ($this->store instanceof AggregateStore) { + if ($this->store->stores()->count() != 1) { + return null; + } + + $store = $this->store->stores()->first(); + } + + if (! $store->indexes()->has($indexName)) { + return null; + } + + return $store->index($indexName); + } + + private function sortUsingIndex($sortIndex, $keys) + { + $indexKeys = $sortIndex->items()->keys(); + $preparedKeys = $this->prepareKeysForOptimizedSort($keys); + $sortKeys = $indexKeys->intersect($preparedKeys->keys()); + + $sortedKeys = []; + + // Reassemble our keys using their indexed order. + // Some builders may change how keys look, and + // we cannot blindly return the index keys. + foreach ($sortKeys as $key) { + $sortedKeys[] = $preparedKeys[$key]; + } + + $sortedKeys = collect($sortedKeys); + + if ($this->orderBys[0]->direction === 'desc') { + $sortedKeys = $sortedKeys->reverse()->values(); + } + + return $sortedKeys; + } + protected function orderKeys($keys) { if ($this->randomize) { @@ -67,6 +122,10 @@ protected function orderKeys($keys) return $keys; } + if ($sortIndex = $this->getOptimizedSortIndex()) { + return $this->sortUsingIndex($sortIndex, $keys); + } + // Get key/value pairs for each orderBy's corresponding index, grouped by index. // eg. [ // 'title' => ['one' => 'One', 'two' => 'Two'], diff --git a/src/Stache/Query/EntryQueryBuilder.php b/src/Stache/Query/EntryQueryBuilder.php index e43fe045b52..a21e4b74ec0 100644 --- a/src/Stache/Query/EntryQueryBuilder.php +++ b/src/Stache/Query/EntryQueryBuilder.php @@ -2,6 +2,7 @@ namespace Statamic\Stache\Query; +use Illuminate\Support\Str; use Statamic\Contracts\Entries\QueryBuilder; use Statamic\Entries\EntryCollection; use Statamic\Facades; @@ -12,6 +13,13 @@ class EntryQueryBuilder extends Builder implements QueryBuilder protected $collections; + protected function prepareKeysForOptimizedSort($keys) + { + return $keys->map(function ($value) { + return Str::after($value, '::'); + })->combine($keys); + } + public function where($column, $operator = null, $value = null, $boolean = 'and') { if ($column === 'collection') { diff --git a/src/Stache/Query/TermQueryBuilder.php b/src/Stache/Query/TermQueryBuilder.php index 3dca2d45cf5..ff4bafb0733 100644 --- a/src/Stache/Query/TermQueryBuilder.php +++ b/src/Stache/Query/TermQueryBuilder.php @@ -2,6 +2,7 @@ namespace Statamic\Stache\Query; +use Illuminate\Support\Str; use Statamic\Facades; use Statamic\Facades\Collection; use Statamic\Taxonomies\TermCollection; @@ -11,6 +12,13 @@ class TermQueryBuilder extends Builder protected $taxonomies; protected $collections; + protected function prepareKeysForOptimizedSort($keys) + { + return $keys->map(function ($value) { + return Str::after($value, '::'); + })->combine($keys); + } + public function where($column, $operator = null, $value = null, $boolean = 'and') { if ($column === 'taxonomy') { diff --git a/tests/Auth/UserGroupTest.php b/tests/Auth/UserGroupTest.php index ecbf76406dc..3440575a2cc 100644 --- a/tests/Auth/UserGroupTest.php +++ b/tests/Auth/UserGroupTest.php @@ -68,7 +68,7 @@ public function it_gets_all_the_users() $userB->addToGroup($group)->save(); $this->assertCount(2, $group->users()); - $this->assertSame([$userA, $userB], $group->users()->all()); + $this->assertSame([$userB, $userA], $group->users()->all()); $this->assertTrue($group->hasUser($userA)); $this->assertTrue($group->hasUser($userB)); } diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index ac60eedbc20..869e895c0da 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -511,7 +511,7 @@ public function entries_are_found_using_where_with_json_value() $this->assertCount(2, $entries); $this->assertEquals(['Post 1', 'Post 5'], $entries->map->title->all()); - $entries = Entry::query()->where('content->value', '<>', 1)->get(); + $entries = Entry::query()->where('content->value', '<>', 1)->orderBy('title')->get(); $this->assertCount(5, $entries); $this->assertEquals(['Post 2', 'Post 3', 'Post 4', 'Post 6', 'Post 7'], $entries->map->title->all()); @@ -719,17 +719,17 @@ public function entries_are_found_using_like($like, $expected) ->create(); }); - $this->assertEquals($expected, Entry::query()->where('title', 'like', $like)->get()->map->title->all()); + $this->assertEquals($expected, Entry::query()->where('title', 'like', $like)->orderBy('title')->get()->map->title->all()); } public static function likeProvider() { return collect([ 'foo' => ['foo'], - 'foo%' => ['foo', 'food', 'foo bar', 'foo_bar', 'foodbar'], + 'foo%' => ['foo', 'foo bar', 'foo_bar', 'food', 'foodbar'], '%world' => ['hello world', 'waterworld'], '%world%' => ['hello world', 'waterworld', 'world of warcraft'], - '_oo' => ['foo', 'boo'], + '_oo' => ['boo', 'foo'], 'o_' => ['on'], 'foo_bar' => ['foo bar', 'foo_bar', 'foodbar'], 'foo__bar' => [], diff --git a/tests/Data/Taxonomies/TermQueryBuilderTest.php b/tests/Data/Taxonomies/TermQueryBuilderTest.php index 2a0c0ce108b..32d58cd3aa1 100644 --- a/tests/Data/Taxonomies/TermQueryBuilderTest.php +++ b/tests/Data/Taxonomies/TermQueryBuilderTest.php @@ -21,8 +21,8 @@ class TermQueryBuilderTest extends TestCase public function it_gets_terms() { Site::setConfig(['sites' => [ - 'en' => ['url' => '/'], - 'fr' => ['url' => '/fr/'], + 'en' => ['url' => '/', 'locale' => 'en_US'], + 'fr' => ['url' => '/fr/', 'locale' => 'fr_FR'], ]]); Taxonomy::make('tags')->sites(['en', 'fr'])->save(); @@ -83,7 +83,7 @@ public function it_filters_using_or_where_ins() Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save(); Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save(); - $terms = Term::query()->whereIn('test', ['foo', 'bar'])->orWhereIn('test', ['foo', 'raz'])->get(); + $terms = Term::query()->whereIn('test', ['foo', 'bar'])->orWhereIn('test', ['foo', 'raz'])->orderBy('slug')->get(); $this->assertEquals(['a', 'b', 'd', 'e'], $terms->map->slug()->values()->all()); } diff --git a/tests/Data/Users/UserQueryBuilderTest.php b/tests/Data/Users/UserQueryBuilderTest.php index b59ef225b7f..13a18dbac95 100644 --- a/tests/Data/Users/UserQueryBuilderTest.php +++ b/tests/Data/Users/UserQueryBuilderTest.php @@ -34,10 +34,10 @@ public function users_are_found_using_or_where_in() User::make()->email('aragorn@precious.com')->data(['name' => 'Aragorn'])->save(); User::make()->email('bombadil@precious.com')->data(['name' => 'Tommy'])->save(); - $users = User::query()->whereIn('name', ['Gandalf', 'Frodo'])->orWhereIn('name', ['Gandalf', 'Aragorn', 'Tommy'])->get(); + $users = User::query()->whereIn('name', ['Gandalf', 'Frodo'])->orWhereIn('name', ['Gandalf', 'Aragorn', 'Tommy'])->orderBy('name')->get(); $this->assertCount(4, $users); - $this->assertEquals(['Gandalf', 'Frodo', 'Aragorn', 'Tommy'], $users->map->name->all()); + $this->assertEquals(['Aragorn', 'Frodo', 'Gandalf', 'Tommy'], $users->map->name->all()); } /** @test **/ @@ -53,7 +53,7 @@ public function users_are_found_using_or_where_not_in() $users = User::query()->whereNotIn('name', ['Gandalf', 'Frodo'])->orWhereNotIn('name', ['Gandalf', 'Sauron'])->get(); $this->assertCount(3, $users); - $this->assertEquals(['Smeagol', 'Aragorn', 'Tommy'], $users->map->name->all()); + $this->assertEquals(['Aragorn', 'Smeagol', 'Tommy'], $users->map->name->all()); } /** @test **/ @@ -119,17 +119,19 @@ public function users_are_found_using_where_with_json_value() $users = User::query() ->where('content->value', 1) + ->orderBy('name') ->get(); $this->assertCount(2, $users); - $this->assertEquals(['Gandalf', 'Aragorn'], $users->map->name->all()); + $this->assertEquals(['Aragorn', 'Gandalf'], $users->map->name->all()); $users = User::query() ->where('content->value', '<>', 1) + ->orderBy('name', 'desc') ->get(); $this->assertCount(6, $users); - $this->assertEquals(['Smeagol', 'Frodo', 'Tommy', 'Sauron', 'Arwen', 'Bilbo'], $users->map->name->all()); + $this->assertEquals(['Tommy', 'Smeagol', 'Sauron', 'Frodo', 'Bilbo', 'Arwen'], $users->map->name->all()); } /** @test **/ @@ -225,7 +227,7 @@ public function users_are_found_using_where_group() $userTwo->addToGroup($groupOne)->save(); $userThree->addToGroup($groupTwo)->save(); - $users = User::query()->whereGroup('one')->get(); + $users = User::query()->whereGroup('one')->orderBy('name')->get(); $this->assertCount(2, $users); $this->assertEquals(['Gandalf', 'Smeagol'], $users->map->name->all()); @@ -302,7 +304,7 @@ public function users_are_found_using_where_role() $userTwo->assignRole($roleOne)->save(); $userThree->assignRole($roleTwo)->save(); - $users = User::query()->whereRole('one')->get(); + $users = User::query()->whereRole('one')->orderBy('name')->get(); $this->assertCount(2, $users); $this->assertEquals(['Gandalf', 'Smeagol'], $users->map->name->all()); diff --git a/tests/Feature/GraphQL/TermsTest.php b/tests/Feature/GraphQL/TermsTest.php index 5e334f14b62..9ff8c4e1b40 100644 --- a/tests/Feature/GraphQL/TermsTest.php +++ b/tests/Feature/GraphQL/TermsTest.php @@ -100,8 +100,8 @@ public function it_queries_all_terms() ['id' => 'tags::bravo', 'title' => 'Tag Bravo'], ['id' => 'categories::alpha', 'title' => 'Category Alpha'], ['id' => 'categories::bravo', 'title' => 'Category Bravo'], - ['id' => 'sizes::small', 'title' => 'Size Small'], ['id' => 'sizes::large', 'title' => 'Size Large'], + ['id' => 'sizes::small', 'title' => 'Size Small'], ]]]]); } @@ -272,8 +272,8 @@ public function it_queries_terms_from_multiple_taxonomies() ->assertExactJson(['data' => ['terms' => ['data' => [ ['id' => 'categories::alpha', 'title' => 'Category Alpha'], ['id' => 'categories::bravo', 'title' => 'Category Bravo'], - ['id' => 'sizes::small', 'title' => 'Size Small'], ['id' => 'sizes::large', 'title' => 'Size Large'], + ['id' => 'sizes::small', 'title' => 'Size Small'], ]]]]); } @@ -327,8 +327,8 @@ public function it_queries_blueprint_specific_fields() ->assertExactJson(['data' => ['terms' => ['data' => [ ['id' => 'tags::alpha', 'foo' => 'FOO!'], ['id' => 'tags::bravo', 'bar' => 'BAR!'], - ['id' => 'sizes::small', 'shorthand' => 'sm'], ['id' => 'sizes::large', 'shorthand' => 'lg'], + ['id' => 'sizes::small', 'shorthand' => 'sm'], ]]]]); } diff --git a/tests/Feature/GraphQL/UsersTest.php b/tests/Feature/GraphQL/UsersTest.php index 812c86dea19..a8e0ae02347 100644 --- a/tests/Feature/GraphQL/UsersTest.php +++ b/tests/Feature/GraphQL/UsersTest.php @@ -220,7 +220,7 @@ public function it_can_filter_users_when_configuration_allows_for_it() contains: "rad", ends_with: "!" } - }) { + }, sort: "id") { data { id bio diff --git a/tests/Feature/Taxonomies/TermEntriesTest.php b/tests/Feature/Taxonomies/TermEntriesTest.php index adf86171fa1..953f8e69ec1 100644 --- a/tests/Feature/Taxonomies/TermEntriesTest.php +++ b/tests/Feature/Taxonomies/TermEntriesTest.php @@ -139,9 +139,9 @@ public function it_gets_and_counts_entries_for_a_localized_term_across_collectio $this->assertEquals(['rouge-shirt'], Term::find('colors::red')->in('fr')->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::black')->in('en')->entriesCount()); - $this->assertEquals(['panther', 'black-shirt'], Term::find('colors::black')->in('en')->entries()->map->slug()->all()); + $this->assertEquals(['black-shirt', 'panther'], Term::find('colors::black')->in('en')->entries()->map->slug()->sort()->values()->all()); $this->assertEquals(2, Term::find('colors::black')->in('fr')->entriesCount()); - $this->assertEquals(['panthere', 'noir-shirt'], Term::find('colors::black')->in('fr')->entries()->map->slug()->all()); + $this->assertEquals(['noir-shirt', 'panthere'], Term::find('colors::black')->in('fr')->entries()->map->slug()->sort()->values()->all()); $this->assertEquals(1, Term::find('colors::yellow')->in('en')->entriesCount()); $this->assertEquals(['cheetah'], Term::find('colors::yellow')->in('en')->entries()->map->slug()->all()); @@ -151,13 +151,13 @@ public function it_gets_and_counts_entries_for_a_localized_term_across_collectio // and for the base Term class, it should not filter by locale $this->assertEquals(2, Term::find('colors::red')->term()->entriesCount()); - $this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->term()->entries()->map->slug()->all()); + $this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->term()->entries()->map->slug()->sort()->values()->all()); $this->assertEquals(4, Term::find('colors::black')->term()->entriesCount()); - $this->assertEquals(['panther', 'panthere', 'black-shirt', 'noir-shirt'], Term::find('colors::black')->term()->entries()->map->slug()->all()); + $this->assertEquals(['black-shirt', 'noir-shirt', 'panther', 'panthere'], Term::find('colors::black')->term()->entries()->map->slug()->sort()->values()->all()); $this->assertEquals(2, Term::find('colors::yellow')->term()->entriesCount()); - $this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->term()->entries()->map->slug()->all()); + $this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->term()->entries()->map->slug()->sort()->values()->all()); } /** @test */ @@ -233,16 +233,16 @@ public function it_gets_and_counts_entries_for_a_localized_term_for_a_single_col $this->assertEquals([], Term::find('colors::red')->collection($animals)->term()->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::black')->collection($animals)->term()->entriesCount()); - $this->assertEquals(['panther', 'panthere'], Term::find('colors::black')->collection($animals)->term()->entries()->map->slug()->all()); + $this->assertEquals(['panther', 'panthere'], Term::find('colors::black')->collection($animals)->term()->entries()->map->slug()->sort()->values()->all()); $this->assertEquals(2, Term::find('colors::yellow')->collection($animals)->term()->entriesCount()); - $this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->collection($animals)->term()->entries()->map->slug()->all()); + $this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->collection($animals)->term()->entries()->map->slug()->sort()->values()->all()); $this->assertEquals(2, Term::find('colors::red')->collection($clothes)->term()->entriesCount()); - $this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->collection($clothes)->term()->entries()->map->slug()->all()); + $this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->collection($clothes)->term()->entries()->map->slug()->sort()->values()->all()); $this->assertEquals(2, Term::find('colors::black')->collection($clothes)->term()->entriesCount()); - $this->assertEquals(['black-shirt', 'noir-shirt'], Term::find('colors::black')->collection($clothes)->term()->entries()->map->slug()->all()); + $this->assertEquals(['black-shirt', 'noir-shirt'], Term::find('colors::black')->collection($clothes)->term()->entries()->map->slug()->sort()->values()->all()); $this->assertEquals(0, Term::find('colors::yellow')->collection($clothes)->term()->entriesCount()); $this->assertEquals([], Term::find('colors::yellow')->collection($clothes)->term()->entries()->map->slug()->all()); From fc47e8029201d8f829fe3f1325d8b2f9c47a9238 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 9 Mar 2024 15:18:46 -0600 Subject: [PATCH 62/81] Revert "Merge branch 'orders-stache-indexes--optimizes-some-sorting' into consolidated-items-wip" This reverts commit 470c7719f89fe353ad91b8ced987f4ebba879df1, reversing changes made to 674ed20e08b524d82c75989bf637c3550cb48403. --- src/Sites/Site.php | 2 +- src/Stache/Indexes/Index.php | 13 ---- src/Stache/Query/Builder.php | 59 ------------------- src/Stache/Query/EntryQueryBuilder.php | 8 --- src/Stache/Query/TermQueryBuilder.php | 8 --- tests/Auth/UserGroupTest.php | 2 +- tests/Data/Entries/EntryQueryBuilderTest.php | 8 +-- .../Data/Taxonomies/TermQueryBuilderTest.php | 6 +- tests/Data/Users/UserQueryBuilderTest.php | 16 +++-- tests/Feature/GraphQL/TermsTest.php | 6 +- tests/Feature/GraphQL/UsersTest.php | 2 +- tests/Feature/Taxonomies/TermEntriesTest.php | 18 +++--- 12 files changed, 29 insertions(+), 119 deletions(-) diff --git a/src/Sites/Site.php b/src/Sites/Site.php index 7470fca6726..655d35c9d36 100644 --- a/src/Sites/Site.php +++ b/src/Sites/Site.php @@ -32,7 +32,7 @@ public function name() public function locale() { - return $this->config['locale'] ?? null; + return $this->config['locale']; } public function shortLocale() diff --git a/src/Stache/Indexes/Index.php b/src/Stache/Indexes/Index.php index 89aec9f4cd6..eb3e57ed59a 100644 --- a/src/Stache/Indexes/Index.php +++ b/src/Stache/Indexes/Index.php @@ -3,7 +3,6 @@ namespace Statamic\Stache\Indexes; use Illuminate\Support\Facades\Cache; -use Statamic\Facades\Compare; use Statamic\Facades\Stache; use Statamic\Statamic; @@ -108,20 +107,8 @@ public function isCached() return Cache::has($this->cacheKey()); } - protected function orderIndex() - { - // Order all indexes in ascending order. If we want - // a descending sort later, we can reverse the - // keys after doing our index comparisons. - uasort($this->items, function ($a, $b) { - return Compare::values($a, $b); - }); - } - public function cache() { - $this->orderIndex(); - Cache::forever($this->cacheKey(), $this->items); } diff --git a/src/Stache/Query/Builder.php b/src/Stache/Query/Builder.php index 9a900c2ae4c..5381cd09cf0 100644 --- a/src/Stache/Query/Builder.php +++ b/src/Stache/Query/Builder.php @@ -5,7 +5,6 @@ use Illuminate\Support\Str; use Statamic\Data\DataCollection; use Statamic\Query\Builder as BaseBuilder; -use Statamic\Stache\Stores\AggregateStore; use Statamic\Stache\Stores\Store; abstract class Builder extends BaseBuilder @@ -86,60 +85,6 @@ public function inRandomOrder() return $this; } - protected function prepareKeysForOptimizedSort($keys) - { - return $keys->combine($keys); - } - - protected function getOptimizedSortIndex() - { - if (count($this->orderBys) != 1) { - return null; - } - - $indexName = $this->orderBys[0]->sort; - - $store = $this->store; - - if ($this->store instanceof AggregateStore) { - if ($this->store->stores()->count() != 1) { - return null; - } - - $store = $this->store->stores()->first(); - } - - if (! $store->indexes()->has($indexName)) { - return null; - } - - return $store->index($indexName); - } - - private function sortUsingIndex($sortIndex, $keys) - { - $indexKeys = $sortIndex->items()->keys(); - $preparedKeys = $this->prepareKeysForOptimizedSort($keys); - $sortKeys = $indexKeys->intersect($preparedKeys->keys()); - - $sortedKeys = []; - - // Reassemble our keys using their indexed order. - // Some builders may change how keys look, and - // we cannot blindly return the index keys. - foreach ($sortKeys as $key) { - $sortedKeys[] = $preparedKeys[$key]; - } - - $sortedKeys = collect($sortedKeys); - - if ($this->orderBys[0]->direction === 'desc') { - $sortedKeys = $sortedKeys->reverse()->values(); - } - - return $sortedKeys; - } - protected function orderKeys($keys) { if ($this->randomize) { @@ -150,10 +95,6 @@ protected function orderKeys($keys) return $keys; } - if ($sortIndex = $this->getOptimizedSortIndex()) { - return $this->sortUsingIndex($sortIndex, $keys); - } - // Get key/value pairs for each orderBy's corresponding index, grouped by index. // eg. [ // 'title' => ['one' => 'One', 'two' => 'Two'], diff --git a/src/Stache/Query/EntryQueryBuilder.php b/src/Stache/Query/EntryQueryBuilder.php index a21e4b74ec0..e43fe045b52 100644 --- a/src/Stache/Query/EntryQueryBuilder.php +++ b/src/Stache/Query/EntryQueryBuilder.php @@ -2,7 +2,6 @@ namespace Statamic\Stache\Query; -use Illuminate\Support\Str; use Statamic\Contracts\Entries\QueryBuilder; use Statamic\Entries\EntryCollection; use Statamic\Facades; @@ -13,13 +12,6 @@ class EntryQueryBuilder extends Builder implements QueryBuilder protected $collections; - protected function prepareKeysForOptimizedSort($keys) - { - return $keys->map(function ($value) { - return Str::after($value, '::'); - })->combine($keys); - } - public function where($column, $operator = null, $value = null, $boolean = 'and') { if ($column === 'collection') { diff --git a/src/Stache/Query/TermQueryBuilder.php b/src/Stache/Query/TermQueryBuilder.php index ff4bafb0733..3dca2d45cf5 100644 --- a/src/Stache/Query/TermQueryBuilder.php +++ b/src/Stache/Query/TermQueryBuilder.php @@ -2,7 +2,6 @@ namespace Statamic\Stache\Query; -use Illuminate\Support\Str; use Statamic\Facades; use Statamic\Facades\Collection; use Statamic\Taxonomies\TermCollection; @@ -12,13 +11,6 @@ class TermQueryBuilder extends Builder protected $taxonomies; protected $collections; - protected function prepareKeysForOptimizedSort($keys) - { - return $keys->map(function ($value) { - return Str::after($value, '::'); - })->combine($keys); - } - public function where($column, $operator = null, $value = null, $boolean = 'and') { if ($column === 'taxonomy') { diff --git a/tests/Auth/UserGroupTest.php b/tests/Auth/UserGroupTest.php index 3440575a2cc..ecbf76406dc 100644 --- a/tests/Auth/UserGroupTest.php +++ b/tests/Auth/UserGroupTest.php @@ -68,7 +68,7 @@ public function it_gets_all_the_users() $userB->addToGroup($group)->save(); $this->assertCount(2, $group->users()); - $this->assertSame([$userB, $userA], $group->users()->all()); + $this->assertSame([$userA, $userB], $group->users()->all()); $this->assertTrue($group->hasUser($userA)); $this->assertTrue($group->hasUser($userB)); } diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index a1e2367323d..43753b0ee27 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -512,7 +512,7 @@ public function entries_are_found_using_where_with_json_value() $this->assertCount(2, $entries); $this->assertEquals(['Post 1', 'Post 5'], $entries->map->title->all()); - $entries = Entry::query()->where('content->value', '<>', 1)->orderBy('title')->get(); + $entries = Entry::query()->where('content->value', '<>', 1)->get(); $this->assertCount(5, $entries); $this->assertEquals(['Post 2', 'Post 3', 'Post 4', 'Post 6', 'Post 7'], $entries->map->title->all()); @@ -720,17 +720,17 @@ public function entries_are_found_using_like($like, $expected) ->create(); }); - $this->assertEquals($expected, Entry::query()->where('title', 'like', $like)->orderBy('title')->get()->map->title->all()); + $this->assertEquals($expected, Entry::query()->where('title', 'like', $like)->get()->map->title->all()); } public static function likeProvider() { return collect([ 'foo' => ['foo'], - 'foo%' => ['foo', 'foo bar', 'foo_bar', 'food', 'foodbar'], + 'foo%' => ['foo', 'food', 'foo bar', 'foo_bar', 'foodbar'], '%world' => ['hello world', 'waterworld'], '%world%' => ['hello world', 'waterworld', 'world of warcraft'], - '_oo' => ['boo', 'foo'], + '_oo' => ['foo', 'boo'], 'o_' => ['on'], 'foo_bar' => ['foo bar', 'foo_bar', 'foodbar'], 'foo__bar' => [], diff --git a/tests/Data/Taxonomies/TermQueryBuilderTest.php b/tests/Data/Taxonomies/TermQueryBuilderTest.php index 32d58cd3aa1..2a0c0ce108b 100644 --- a/tests/Data/Taxonomies/TermQueryBuilderTest.php +++ b/tests/Data/Taxonomies/TermQueryBuilderTest.php @@ -21,8 +21,8 @@ class TermQueryBuilderTest extends TestCase public function it_gets_terms() { Site::setConfig(['sites' => [ - 'en' => ['url' => '/', 'locale' => 'en_US'], - 'fr' => ['url' => '/fr/', 'locale' => 'fr_FR'], + 'en' => ['url' => '/'], + 'fr' => ['url' => '/fr/'], ]]); Taxonomy::make('tags')->sites(['en', 'fr'])->save(); @@ -83,7 +83,7 @@ public function it_filters_using_or_where_ins() Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save(); Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save(); - $terms = Term::query()->whereIn('test', ['foo', 'bar'])->orWhereIn('test', ['foo', 'raz'])->orderBy('slug')->get(); + $terms = Term::query()->whereIn('test', ['foo', 'bar'])->orWhereIn('test', ['foo', 'raz'])->get(); $this->assertEquals(['a', 'b', 'd', 'e'], $terms->map->slug()->values()->all()); } diff --git a/tests/Data/Users/UserQueryBuilderTest.php b/tests/Data/Users/UserQueryBuilderTest.php index 13a18dbac95..b59ef225b7f 100644 --- a/tests/Data/Users/UserQueryBuilderTest.php +++ b/tests/Data/Users/UserQueryBuilderTest.php @@ -34,10 +34,10 @@ public function users_are_found_using_or_where_in() User::make()->email('aragorn@precious.com')->data(['name' => 'Aragorn'])->save(); User::make()->email('bombadil@precious.com')->data(['name' => 'Tommy'])->save(); - $users = User::query()->whereIn('name', ['Gandalf', 'Frodo'])->orWhereIn('name', ['Gandalf', 'Aragorn', 'Tommy'])->orderBy('name')->get(); + $users = User::query()->whereIn('name', ['Gandalf', 'Frodo'])->orWhereIn('name', ['Gandalf', 'Aragorn', 'Tommy'])->get(); $this->assertCount(4, $users); - $this->assertEquals(['Aragorn', 'Frodo', 'Gandalf', 'Tommy'], $users->map->name->all()); + $this->assertEquals(['Gandalf', 'Frodo', 'Aragorn', 'Tommy'], $users->map->name->all()); } /** @test **/ @@ -53,7 +53,7 @@ public function users_are_found_using_or_where_not_in() $users = User::query()->whereNotIn('name', ['Gandalf', 'Frodo'])->orWhereNotIn('name', ['Gandalf', 'Sauron'])->get(); $this->assertCount(3, $users); - $this->assertEquals(['Aragorn', 'Smeagol', 'Tommy'], $users->map->name->all()); + $this->assertEquals(['Smeagol', 'Aragorn', 'Tommy'], $users->map->name->all()); } /** @test **/ @@ -119,19 +119,17 @@ public function users_are_found_using_where_with_json_value() $users = User::query() ->where('content->value', 1) - ->orderBy('name') ->get(); $this->assertCount(2, $users); - $this->assertEquals(['Aragorn', 'Gandalf'], $users->map->name->all()); + $this->assertEquals(['Gandalf', 'Aragorn'], $users->map->name->all()); $users = User::query() ->where('content->value', '<>', 1) - ->orderBy('name', 'desc') ->get(); $this->assertCount(6, $users); - $this->assertEquals(['Tommy', 'Smeagol', 'Sauron', 'Frodo', 'Bilbo', 'Arwen'], $users->map->name->all()); + $this->assertEquals(['Smeagol', 'Frodo', 'Tommy', 'Sauron', 'Arwen', 'Bilbo'], $users->map->name->all()); } /** @test **/ @@ -227,7 +225,7 @@ public function users_are_found_using_where_group() $userTwo->addToGroup($groupOne)->save(); $userThree->addToGroup($groupTwo)->save(); - $users = User::query()->whereGroup('one')->orderBy('name')->get(); + $users = User::query()->whereGroup('one')->get(); $this->assertCount(2, $users); $this->assertEquals(['Gandalf', 'Smeagol'], $users->map->name->all()); @@ -304,7 +302,7 @@ public function users_are_found_using_where_role() $userTwo->assignRole($roleOne)->save(); $userThree->assignRole($roleTwo)->save(); - $users = User::query()->whereRole('one')->orderBy('name')->get(); + $users = User::query()->whereRole('one')->get(); $this->assertCount(2, $users); $this->assertEquals(['Gandalf', 'Smeagol'], $users->map->name->all()); diff --git a/tests/Feature/GraphQL/TermsTest.php b/tests/Feature/GraphQL/TermsTest.php index 9ff8c4e1b40..5e334f14b62 100644 --- a/tests/Feature/GraphQL/TermsTest.php +++ b/tests/Feature/GraphQL/TermsTest.php @@ -100,8 +100,8 @@ public function it_queries_all_terms() ['id' => 'tags::bravo', 'title' => 'Tag Bravo'], ['id' => 'categories::alpha', 'title' => 'Category Alpha'], ['id' => 'categories::bravo', 'title' => 'Category Bravo'], - ['id' => 'sizes::large', 'title' => 'Size Large'], ['id' => 'sizes::small', 'title' => 'Size Small'], + ['id' => 'sizes::large', 'title' => 'Size Large'], ]]]]); } @@ -272,8 +272,8 @@ public function it_queries_terms_from_multiple_taxonomies() ->assertExactJson(['data' => ['terms' => ['data' => [ ['id' => 'categories::alpha', 'title' => 'Category Alpha'], ['id' => 'categories::bravo', 'title' => 'Category Bravo'], - ['id' => 'sizes::large', 'title' => 'Size Large'], ['id' => 'sizes::small', 'title' => 'Size Small'], + ['id' => 'sizes::large', 'title' => 'Size Large'], ]]]]); } @@ -327,8 +327,8 @@ public function it_queries_blueprint_specific_fields() ->assertExactJson(['data' => ['terms' => ['data' => [ ['id' => 'tags::alpha', 'foo' => 'FOO!'], ['id' => 'tags::bravo', 'bar' => 'BAR!'], - ['id' => 'sizes::large', 'shorthand' => 'lg'], ['id' => 'sizes::small', 'shorthand' => 'sm'], + ['id' => 'sizes::large', 'shorthand' => 'lg'], ]]]]); } diff --git a/tests/Feature/GraphQL/UsersTest.php b/tests/Feature/GraphQL/UsersTest.php index a8e0ae02347..812c86dea19 100644 --- a/tests/Feature/GraphQL/UsersTest.php +++ b/tests/Feature/GraphQL/UsersTest.php @@ -220,7 +220,7 @@ public function it_can_filter_users_when_configuration_allows_for_it() contains: "rad", ends_with: "!" } - }, sort: "id") { + }) { data { id bio diff --git a/tests/Feature/Taxonomies/TermEntriesTest.php b/tests/Feature/Taxonomies/TermEntriesTest.php index 953f8e69ec1..adf86171fa1 100644 --- a/tests/Feature/Taxonomies/TermEntriesTest.php +++ b/tests/Feature/Taxonomies/TermEntriesTest.php @@ -139,9 +139,9 @@ public function it_gets_and_counts_entries_for_a_localized_term_across_collectio $this->assertEquals(['rouge-shirt'], Term::find('colors::red')->in('fr')->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::black')->in('en')->entriesCount()); - $this->assertEquals(['black-shirt', 'panther'], Term::find('colors::black')->in('en')->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['panther', 'black-shirt'], Term::find('colors::black')->in('en')->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::black')->in('fr')->entriesCount()); - $this->assertEquals(['noir-shirt', 'panthere'], Term::find('colors::black')->in('fr')->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['panthere', 'noir-shirt'], Term::find('colors::black')->in('fr')->entries()->map->slug()->all()); $this->assertEquals(1, Term::find('colors::yellow')->in('en')->entriesCount()); $this->assertEquals(['cheetah'], Term::find('colors::yellow')->in('en')->entries()->map->slug()->all()); @@ -151,13 +151,13 @@ public function it_gets_and_counts_entries_for_a_localized_term_across_collectio // and for the base Term class, it should not filter by locale $this->assertEquals(2, Term::find('colors::red')->term()->entriesCount()); - $this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->term()->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->term()->entries()->map->slug()->all()); $this->assertEquals(4, Term::find('colors::black')->term()->entriesCount()); - $this->assertEquals(['black-shirt', 'noir-shirt', 'panther', 'panthere'], Term::find('colors::black')->term()->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['panther', 'panthere', 'black-shirt', 'noir-shirt'], Term::find('colors::black')->term()->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::yellow')->term()->entriesCount()); - $this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->term()->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->term()->entries()->map->slug()->all()); } /** @test */ @@ -233,16 +233,16 @@ public function it_gets_and_counts_entries_for_a_localized_term_for_a_single_col $this->assertEquals([], Term::find('colors::red')->collection($animals)->term()->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::black')->collection($animals)->term()->entriesCount()); - $this->assertEquals(['panther', 'panthere'], Term::find('colors::black')->collection($animals)->term()->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['panther', 'panthere'], Term::find('colors::black')->collection($animals)->term()->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::yellow')->collection($animals)->term()->entriesCount()); - $this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->collection($animals)->term()->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->collection($animals)->term()->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::red')->collection($clothes)->term()->entriesCount()); - $this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->collection($clothes)->term()->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->collection($clothes)->term()->entries()->map->slug()->all()); $this->assertEquals(2, Term::find('colors::black')->collection($clothes)->term()->entriesCount()); - $this->assertEquals(['black-shirt', 'noir-shirt'], Term::find('colors::black')->collection($clothes)->term()->entries()->map->slug()->sort()->values()->all()); + $this->assertEquals(['black-shirt', 'noir-shirt'], Term::find('colors::black')->collection($clothes)->term()->entries()->map->slug()->all()); $this->assertEquals(0, Term::find('colors::yellow')->collection($clothes)->term()->entriesCount()); $this->assertEquals([], Term::find('colors::yellow')->collection($clothes)->term()->entries()->map->slug()->all()); From 71a910e507be256b3e7e2109009468c566328a3e Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 9 Mar 2024 16:16:58 -0600 Subject: [PATCH 63/81] Adjust sorting and comparison implementation --- src/Data/DataCollection.php | 23 +++++++++++++++-------- src/Support/Comparator.php | 12 +++++++++--- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Data/DataCollection.php b/src/Data/DataCollection.php index a3a6dea716c..c2264d1bf40 100644 --- a/src/Data/DataCollection.php +++ b/src/Data/DataCollection.php @@ -41,21 +41,28 @@ public function multisort($sort) } $sorts = explode('|', $sort); + $preparedSorts = []; + + foreach ($sorts as $sort) { + $bits = explode(':', $sort); + $sortBy = $bits[0]; + $sortDir = $bits[1] ?? null; + + $preparedSorts[$sortBy] = $sortDir === 'desc'; + } $arr = $this->all(); - uasort($arr, function ($a, $b) use ($sorts) { - foreach ($sorts as $sort) { - $bits = explode(':', $sort); - $sort_by = $bits[0]; - $sort_dir = array_get($bits, 1); + $comparator = Compare::getFacadeRoot(); - [$one, $two] = $this->getSortableValues($sort_by, $a, $b); + uasort($arr, function ($a, $b) use ($comparator, $preparedSorts) { + foreach ($preparedSorts as $sortBy => $sortDir) { + [$one, $two] = $this->getSortableValues($sortBy, $a, $b); - $result = Compare::values($one, $two); + $result = $comparator->values($one, $two); if ($result !== 0) { - return ($sort_dir === 'desc') ? $result * -1 : $result; + return ($sortDir === true) ? $result * -1 : $result; } } diff --git a/src/Support/Comparator.php b/src/Support/Comparator.php index 34aed0e838a..863a87e8f84 100644 --- a/src/Support/Comparator.php +++ b/src/Support/Comparator.php @@ -8,10 +8,16 @@ class Comparator { protected $locale; + protected static $hasCheckedCollator = false; + protected static $canUseCollator = false; public function __construct() { $this->locale = Site::current()->locale(); + + if (! self::$hasCheckedCollator) { + self::$canUseCollator = class_exists(Collator::class); + } } public function locale($locale) @@ -85,10 +91,10 @@ public function values($one, $two): int */ public function strings(string $one, string $two): int { - $one = Str::lower($one); - $two = Str::lower($two); + $one = mb_strtolower($one); + $two = mb_strtolower($two); - if (! class_exists(Collator::class)) { + if (! self::$canUseCollator) { return strcmp($one, $two); } From e26c73a0a739c2b2913e115eb5112989182d2419 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 9 Mar 2024 20:50:25 -0600 Subject: [PATCH 64/81] Refactor Globals to locate items for a set using the Stache --- .../Repositories/GlobalVariablesRepository.php | 18 +++++++++++++----- src/Stache/Stores/GlobalVariablesStore.php | 7 +++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Stache/Repositories/GlobalVariablesRepository.php b/src/Stache/Repositories/GlobalVariablesRepository.php index 7e8608f132b..8f93100a34c 100644 --- a/src/Stache/Repositories/GlobalVariablesRepository.php +++ b/src/Stache/Repositories/GlobalVariablesRepository.php @@ -6,7 +6,6 @@ use Statamic\Contracts\Globals\Variables; use Statamic\Globals\VariablesCollection; use Statamic\Stache\Stache; -use Statamic\Support\Str; class GlobalVariablesRepository implements RepositoryContract { @@ -31,12 +30,21 @@ public function find($id): ?Variables return $this->store->getItem($id); } + private function getIdsForSet($handle) + { + return $this->store + ->index('handle') + ->items() + ->where(function ($value) use ($handle) { + return $value == $handle; + })->keys()->all(); + } + public function whereSet($handle): VariablesCollection { - return $this - ->all() - ->filter(fn ($variable) => Str::before($variable->id(), '::') == $handle) - ->values(); + return new VariablesCollection( + $this->store->getItems($this->getIdsForSet($handle)) + ); } public function save($variable) diff --git a/src/Stache/Stores/GlobalVariablesStore.php b/src/Stache/Stores/GlobalVariablesStore.php index 77096a34313..331324679ee 100644 --- a/src/Stache/Stores/GlobalVariablesStore.php +++ b/src/Stache/Stores/GlobalVariablesStore.php @@ -84,4 +84,11 @@ protected function deleteItemFromDisk($item) $item->globalSet()->removeLocalization($item)->writeFile(); } } + + protected function storeIndexes() + { + return [ + 'handle', + ]; + } } From ebb21b52afbf334b4fbfaa3534c70d0a42f035f4 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 9 Mar 2024 20:52:26 -0600 Subject: [PATCH 65/81] Add ability to specify which stores to warm --- src/Console/Commands/StacheWarm.php | 8 ++++++-- src/Facades/Stache.php | 2 +- src/Stache/Stache.php | 10 ++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Console/Commands/StacheWarm.php b/src/Console/Commands/StacheWarm.php index 7df4777b2d2..d0e0f5d6a33 100644 --- a/src/Console/Commands/StacheWarm.php +++ b/src/Console/Commands/StacheWarm.php @@ -11,7 +11,7 @@ class StacheWarm extends Command { use RunsInPlease; - protected $signature = 'statamic:stache:warm'; + protected $signature = 'statamic:stache:warm {store?}'; protected $description = 'Build the "Stache" cache'; public function handle() @@ -20,7 +20,11 @@ public function handle() $this->line('Please wait. This may take a while if you have a lot of content.'); - Stache::warm(); + if ($stores = $this->argument('store')) { + $stores = collect(explode(',', $stores))->map(fn ($store) => trim($store))->all(); + } + + Stache::warm($stores ?? []); $this->info('You have poured oil over the Stache and polished it until it shines. It is warm and ready'); } diff --git a/src/Facades/Stache.php b/src/Facades/Stache.php index 6a321cbd1a9..359372796f0 100644 --- a/src/Facades/Stache.php +++ b/src/Facades/Stache.php @@ -14,7 +14,7 @@ * @method static string generateId() * @method static self clear() * @method static void refresh() - * @method static void warm() + * @method static void warm($stores = []) * @method static self instance() * @method static mixed fileCount() * @method static mixed|null fileSize() diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index 1bfab301e9f..5ac13176a3e 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -98,7 +98,7 @@ public function refresh() return $this->clear()->warm(); } - public function warm() + public function warm($stores = []) { Partyline::comment('Warming Stache...'); @@ -106,7 +106,13 @@ public function warm() $this->startTimer(); - $this->stores()->each->warm(); + $this->stores()->where(function ($store, $key) use ($stores) { + if (count($stores) == 0) { + return true; + } + + return in_array($key, $stores); + })->each->warm(); $this->stopTimer(); From c72935c9f249b2727336681632c48052f8e69371 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 10 Mar 2024 14:40:27 -0500 Subject: [PATCH 66/81] Adds support for logging Stache queries --- config/stache.php | 16 ++ src/Query/Builder.php | 3 + src/Stache/Query/Builder.php | 12 +- src/Stache/Query/Concerns/DumpsQueryParts.php | 51 ++++++ .../Query/Concerns/DumpsQueryValues.php | 60 +++++++ src/Stache/Query/Concerns/DumpsWheres.php | 162 ++++++++++++++++++ .../Query/Concerns/LogsStacheQueries.php | 41 +++++ src/Stache/Query/StacheQueryDumper.php | 48 ++++++ 8 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 src/Stache/Query/Concerns/DumpsQueryParts.php create mode 100644 src/Stache/Query/Concerns/DumpsQueryValues.php create mode 100644 src/Stache/Query/Concerns/DumpsWheres.php create mode 100644 src/Stache/Query/Concerns/LogsStacheQueries.php create mode 100644 src/Stache/Query/StacheQueryDumper.php diff --git a/config/stache.php b/config/stache.php index e0db82e05ca..86c75d81bff 100644 --- a/config/stache.php +++ b/config/stache.php @@ -124,4 +124,20 @@ 'timeout' => 30, ], + /* + |-------------------------------------------------------------------------- + | Query Logging + |-------------------------------------------------------------------------- + | + | When enabled, the Stache query builders will log queries + | like normal SQL Queries. To log actual query values, + | set the dump_values configuration option to true. + | + */ + + 'query_logging' => [ + 'enabled' => true, + 'dump_values' => false, + ], + ]; diff --git a/src/Query/Builder.php b/src/Query/Builder.php index ec7dbaca0ff..19a2720dcf6 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -12,9 +12,12 @@ use Statamic\Contracts\Query\Builder as Contract; use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Pattern; +use Statamic\Stache\Query\Concerns\LogsStacheQueries; abstract class Builder implements Contract { + use LogsStacheQueries; + protected $columns; protected $limit; protected $offset = 0; diff --git a/src/Stache/Query/Builder.php b/src/Stache/Query/Builder.php index 139d5553d7f..71dff28aa99 100644 --- a/src/Stache/Query/Builder.php +++ b/src/Stache/Query/Builder.php @@ -14,6 +14,8 @@ abstract class Builder extends BaseBuilder public function __construct(Store $store) { $this->store = $store; + $this->loggerEnabled = config('statamic.stache.query_logging.enabled', false); + $this->logRealValues = config('statamic.stache.query_logging.dump_values', false); } public function count() @@ -23,6 +25,8 @@ public function count() public function get($columns = ['*']) { + $startTime = hrtime(true); + $keys = $this->getFilteredKeys(); $keys = $this->orderKeys($keys); @@ -35,7 +39,13 @@ public function get($columns = ['*']) ->selectedQueryColumns($this->columns ?? $columns) ->selectedQueryRelations($this->with)); - return $this->collect($items)->values(); + $values = $this->collect($items)->values(); + + $endTime = hrtime(true); + + $this->emitQueryEvent($startTime, $endTime); + + return $values; } abstract protected function getFilteredKeys(); diff --git a/src/Stache/Query/Concerns/DumpsQueryParts.php b/src/Stache/Query/Concerns/DumpsQueryParts.php new file mode 100644 index 00000000000..e7586f281eb --- /dev/null +++ b/src/Stache/Query/Concerns/DumpsQueryParts.php @@ -0,0 +1,51 @@ +columns != null) { + $columns = implode(', ', $this->columns); + } + + return $columns; + } + + protected function dumpLimits(): string + { + if (! $this->limit) { + return ''; + } + + $limit = "\n".'LIMIT '.$this->limit; + + if ($this->offset) { + $limit .= ' OFFSET '.$this->offset; + } + + return $limit; + } + + protected function dumpOrderBys(): string + { + if (count($this->orderBys) === 0) { + return ''; + } + + $orders = []; + + foreach ($this->orderBys as $orderBy) { + if (! $orderBy->sort) { + continue; + } + + $orders[] = $orderBy->sort.' '.strtoupper($orderBy->direction); + } + + return "\n".'ORDER BY '.implode(', ', $orders); + } +} diff --git a/src/Stache/Query/Concerns/DumpsQueryValues.php b/src/Stache/Query/Concerns/DumpsQueryValues.php new file mode 100644 index 00000000000..a97b81bc5c8 --- /dev/null +++ b/src/Stache/Query/Concerns/DumpsQueryValues.php @@ -0,0 +1,60 @@ +dumpActualValues) { + return implode(', ', array_fill(0, count($array), '?')); + } + + return collect($array)->map(function ($value) { + return $this->dumpQueryValue($value); + })->implode(', '); + } + + protected function dumpQueryValue($value): string + { + if (! $this->dumpActualValues) { + return '?'; + } + + if (is_string($value)) { + return "'$value'"; + } + + if (is_bool($value)) { + if ($value === true) { + return '1'; + } + + return '0'; + } + + if (is_null($value)) { + return 'NULL'; + } + + if ($value instanceof Carbon) { + return $value->toIso8601String(); + } + + if (is_numeric($value)) { + return (string) $value; + } + + if (is_object($value)) { + return '{object}'; + } + + return '{value}'; + } +} diff --git a/src/Stache/Query/Concerns/DumpsWheres.php b/src/Stache/Query/Concerns/DumpsWheres.php new file mode 100644 index 00000000000..f13a3e67ea8 --- /dev/null +++ b/src/Stache/Query/Concerns/DumpsWheres.php @@ -0,0 +1,162 @@ +dumpQueryValue($where['value'] ?? null); + } + + protected function dumpArrayWhere($keyword, $where): string + { + return $where['column'].' '.$keyword.' ('.$this->dumpQueryArrayValues($where['values'] ?? []).')'; + } + + protected function dumpSimpleOperatorWhere($where): string + { + return $where['column'].' '.$where['operator'].$this->dumpQueryValue($where['value'] ?? null); + } + + protected function dumpIn($where): string + { + return $this->dumpArrayWhere('IN', $where); + } + + protected function dumpNotIn($where): string + { + return $this->dumpArrayWhere('NOT IN', $where); + } + + protected function dumpNull($where): string + { + return $where['column'].' IS NULL'; + } + + protected function dumpNotNull($where): string + { + return $where['column'].' IS NOT NULL'; + } + + protected function dumpDatePartMethod($datePart, $where): string + { + return 'DATEPART('.$datePart.', '.$where['column'].') = '.$this->dumpQueryValue($where['value'] ?? null); + } + + protected function dumpMonth($where): string + { + return $this->dumpDatePartMethod('MONTH', $where); + } + + protected function dumpDay($where): string + { + return $this->dumpDatePartMethod('DAY', $where); + } + + protected function dumpYear($where): string + { + return $this->dumpDatePartMethod('YEAR', $where); + } + + protected function dumpTime($where): string + { + return $this->dumpDatePartMethod('TIMESTAMP', $where); + } + + protected function dumpBetween($where): string + { + $valueOne = $this->dumpQueryValue($where['values'][0] ?? null); + $valueTwo = $this->dumpQueryValue($where['values'][1] ?? null); + $column = $where['column']; + + return $column.' BETWEEN '.$valueOne.' AND '.$valueTwo; + } + + protected function dumpNotBetween($where): string + { + $valueOne = $this->dumpQueryValue($where['values'][0] ?? null); + $valueTwo = $this->dumpQueryValue($where['values'][1] ?? null); + $column = $where['column']; + + return $column.' NOT BETWEEN '.$valueOne.' AND '.$valueTwo; + } + + protected function dumpColumn($where): string + { + return $where['column'].' = '.$where['value']; + } + + protected function dumpNested($where): string + { + $query = $where['query'] ?? null; + + if (! $query instanceof Builder) { + return ''; + } + + return '('.$query->dumpStacheQuery().')'; + } + + protected function dumpDate($where) + { + return $this->dumpSimpleOperatorWhere($where); + } + + protected function dumpJsonMethod($where): string + { + $jsonMethod = strtoupper(Str::snake($where['type'])); + + if (isset($where['values'])) { + $valueString = $this->dumpQueryArrayValues($where['values']); + } else { + $valueString = $this->dumpQueryValue($where['value'] ?? null); + } + + return $jsonMethod.'('.$where['column'].', '.$valueString.')'; + } + + protected function dumpWhere($isFirst, $where): string + { + $dumpedWhere = ''; + + if (! $isFirst) { + $dumpedWhere = strtoupper($where['boolean']).' '; + } + + $type = $where['type']; + + if (Str::startsWith($type, 'Json')) { + $dumpedWhere .= $this->dumpJsonMethod($where); + } else { + $whereMethod = 'dump'.ucfirst($type); + + if (method_exists($this, $whereMethod)) { + $dumpedWhere .= $this->{$whereMethod}($where); + } else { + // Fail-safe to dump "something". + $dumpedWhere .= strtoupper($type); + } + } + + return $dumpedWhere; + } + + protected function dumpWheres(): string + { + if (count($this->wheres) === 0) { + return ''; + } + + $parts = []; + + for ($i = 0; $i < count($this->wheres); $i++) { + $parts[] = $this->dumpWhere($i === 0, $this->wheres[$i]); + } + + return "\n".'WHERE '.implode(' ', $parts); + } +} diff --git a/src/Stache/Query/Concerns/LogsStacheQueries.php b/src/Stache/Query/Concerns/LogsStacheQueries.php new file mode 100644 index 00000000000..d9e331eff35 --- /dev/null +++ b/src/Stache/Query/Concerns/LogsStacheQueries.php @@ -0,0 +1,41 @@ +wheres, + $this->columns, + $this->orderBys, + $this->limit, + $this->offset + )) + ->setDumpActualValues($this->logRealValues) + ->dump(); + } + + protected function emitQueryEvent($startTime, $endTime): void + { + if (! $this->loggerEnabled) { + return; + } + + event(new QueryExecuted( + $this->dumpStacheQuery(), + [], + ($endTime - $startTime) / 1000000, + new Connection(fn () => null, 'Stache') + )); + } +} diff --git a/src/Stache/Query/StacheQueryDumper.php b/src/Stache/Query/StacheQueryDumper.php new file mode 100644 index 00000000000..2b2447a560c --- /dev/null +++ b/src/Stache/Query/StacheQueryDumper.php @@ -0,0 +1,48 @@ +store = $store; + $this->wheres = $wheres; + $this->columns = $columns; + $this->orderBys = $orderBys; + $this->limit = $limit; + $this->offset = $offset; + } + + public function setDumpActualValues($dumpValues): self + { + $this->dumpActualValues = $dumpValues; + + return $this; + } + + public function dump(): string + { + $query = 'SELECT '.$this->dumpColumns()."\n".'FROM '.get_class($this->store); + $query .= $this->dumpWheres(); + $query .= $this->dumpLimits(); + $query .= $this->dumpOrderBys(); + + return $query; + } +} From 18180062cde7067895ce066d1691a013c690ad66 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 10 Mar 2024 14:49:24 -0500 Subject: [PATCH 67/81] Update stache.php --- config/stache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/stache.php b/config/stache.php index 86c75d81bff..48ede4a4387 100644 --- a/config/stache.php +++ b/config/stache.php @@ -136,7 +136,7 @@ */ 'query_logging' => [ - 'enabled' => true, + 'enabled' => false, 'dump_values' => false, ], From a6f5ce6e58221c617d0eecc66f4474868eeda53b Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 10 Mar 2024 15:03:17 -0500 Subject: [PATCH 68/81] Update Builder.php --- src/Stache/Query/Builder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stache/Query/Builder.php b/src/Stache/Query/Builder.php index 3a51dba5efc..8b0094be710 100644 --- a/src/Stache/Query/Builder.php +++ b/src/Stache/Query/Builder.php @@ -26,8 +26,6 @@ public function count() protected function resolveKeys() { - $startTime = hrtime(true); - $keys = $this->getFilteredKeys(); $keys = $this->orderKeys($keys); @@ -61,6 +59,8 @@ protected function getKeysForIndexQuery($keys) public function get($columns = ['*']) { + $startTime = hrtime(true); + $items = $this->getItems($this->resolveKeys()); $items->each(fn ($item) => $item From 4f3ea3a0545196366798b5e3a29f2a7ca9df5c70 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 10 Mar 2024 15:20:01 -0500 Subject: [PATCH 69/81] Update LogsStacheQueries.php --- src/Stache/Query/Concerns/LogsStacheQueries.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stache/Query/Concerns/LogsStacheQueries.php b/src/Stache/Query/Concerns/LogsStacheQueries.php index d9e331eff35..61b9fe58650 100644 --- a/src/Stache/Query/Concerns/LogsStacheQueries.php +++ b/src/Stache/Query/Concerns/LogsStacheQueries.php @@ -14,7 +14,7 @@ trait LogsStacheQueries public function dumpStacheQuery() { return (new StacheQueryDumper( - $this, + $this->store, $this->wheres, $this->columns, $this->orderBys, From c52423098b0b7dc275ec09cf79ef03e300b4cce7 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 10 Mar 2024 15:25:23 -0500 Subject: [PATCH 70/81] Add extra FROM output from entry store --- src/Stache/Query/Concerns/LogsStacheQueries.php | 10 ++++++++++ src/Stache/Query/StacheQueryDumper.php | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/Stache/Query/Concerns/LogsStacheQueries.php b/src/Stache/Query/Concerns/LogsStacheQueries.php index 61b9fe58650..c15432cea72 100644 --- a/src/Stache/Query/Concerns/LogsStacheQueries.php +++ b/src/Stache/Query/Concerns/LogsStacheQueries.php @@ -4,6 +4,7 @@ use Illuminate\Database\Connection; use Illuminate\Database\Events\QueryExecuted; +use Statamic\Stache\Query\EntryQueryBuilder; use Statamic\Stache\Query\StacheQueryDumper; trait LogsStacheQueries @@ -13,6 +14,14 @@ trait LogsStacheQueries public function dumpStacheQuery() { + $extraFrom = ''; + + if ($this instanceof EntryQueryBuilder) { + if (is_array($this->collections)) { + $extraFrom = implode(', ', $this->collections); + } + } + return (new StacheQueryDumper( $this->store, $this->wheres, @@ -22,6 +31,7 @@ public function dumpStacheQuery() $this->offset )) ->setDumpActualValues($this->logRealValues) + ->setExtraFromStatement($extraFrom) ->dump(); } diff --git a/src/Stache/Query/StacheQueryDumper.php b/src/Stache/Query/StacheQueryDumper.php index 2b2447a560c..220af3fc36c 100644 --- a/src/Stache/Query/StacheQueryDumper.php +++ b/src/Stache/Query/StacheQueryDumper.php @@ -16,6 +16,7 @@ class StacheQueryDumper protected $limit; protected $offset; protected $store; + protected $extraFrom = ''; protected $dumpActualValues = false; public function __construct( @@ -29,6 +30,13 @@ public function __construct( $this->offset = $offset; } + public function setExtraFromStatement($extraFrom): self + { + $this->extraFrom = $extraFrom; + + return $this; + } + public function setDumpActualValues($dumpValues): self { $this->dumpActualValues = $dumpValues; @@ -39,6 +47,11 @@ public function setDumpActualValues($dumpValues): self public function dump(): string { $query = 'SELECT '.$this->dumpColumns()."\n".'FROM '.get_class($this->store); + + if ($this->extraFrom) { + $query .= '{'.$this->extraFrom.'}'; + } + $query .= $this->dumpWheres(); $query .= $this->dumpLimits(); $query .= $this->dumpOrderBys(); From a3c32c1e9483b47ba3fc77166dca73f4a978eb76 Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 12 Mar 2024 19:26:09 -0500 Subject: [PATCH 71/81] Refactor hasOrigin() cached call --- src/Data/HasOrigin.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index e2cbf854b54..f1cf7c07c18 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -10,7 +10,6 @@ trait HasOrigin * @var string */ protected $origin; - protected $cachedHasOrigin = false; public function keys() { @@ -68,7 +67,6 @@ public function origin($origin = null) Blink::forget($this->getOriginBlinkKey()); $this->origin = is_object($origin) ? $this->getOriginIdFromObject($origin) : $origin; - $this->cachedHasOrigin = $this->origin != null; return $this; } @@ -87,11 +85,7 @@ protected function getOriginIdFromObject($origin) public function hasOrigin() { - if (! $this->cachedHasOrigin && $this->origin) { - $this->cachedHasOrigin = true; - } - - return $this->cachedHasOrigin; + return $this->origin != null; } public function isRoot() From d5ced64793af61b4c049a1efe4e7fdc88810fb3b Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 12 Mar 2024 19:36:09 -0500 Subject: [PATCH 72/81] Update HasOrigin.php --- src/Data/HasOrigin.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 4510233ddef..ebdaf9437ce 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -64,10 +64,6 @@ public function value($key) public function origin($origin = null) { if (func_num_args() === 0) { - if (! $origin) { - return null; - } - if ($found = Blink::get($this->getOriginBlinkKey())) { return $found; } From ad4285139ae6661b8bad62d98a8e78f03b78b1b8 Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 12 Mar 2024 19:51:12 -0500 Subject: [PATCH 73/81] Revert "Update HasOrigin.php" This reverts commit d5ced64793af61b4c049a1efe4e7fdc88810fb3b. --- src/Data/HasOrigin.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index ebdaf9437ce..4510233ddef 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -64,6 +64,10 @@ public function value($key) public function origin($origin = null) { if (func_num_args() === 0) { + if (! $origin) { + return null; + } + if ($found = Blink::get($this->getOriginBlinkKey())) { return $found; } From 6a02ad4ba3d288554f9fd5ac96ebe127345f1915 Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 12 Mar 2024 20:05:34 -0500 Subject: [PATCH 74/81] =?UTF-8?q?"$this->"=20was=20important=20?= =?UTF-8?q?=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Data/HasOrigin.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 4510233ddef..33c0de88c57 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -64,7 +64,7 @@ public function value($key) public function origin($origin = null) { if (func_num_args() === 0) { - if (! $origin) { + if (! $this->origin) { return null; } @@ -72,7 +72,7 @@ public function origin($origin = null) return $found; } - return tap($this->getOriginByString($origin), function ($found) { + return tap($this->getOriginByString($this->origin), function ($found) { Blink::put($this->getOriginBlinkKey(), $found); }); } @@ -98,7 +98,7 @@ protected function getOriginIdFromObject($origin) public function hasOrigin() { - return $this->origin != null; + return $this->origin !== null; } public function isRoot() From cc1f7224408f6ced8da05224cdc54a938d6eded6 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 24 Mar 2024 12:28:04 -0500 Subject: [PATCH 75/81] Still call method in the event we haven't actually resolved the origin yet --- src/Data/HasOrigin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 33c0de88c57..fe045594ee8 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -98,7 +98,7 @@ protected function getOriginIdFromObject($origin) public function hasOrigin() { - return $this->origin !== null; + return $this->origin() !== null; } public function isRoot() From afd7e96c8e1ebb41cb2defd06832a5273c2af1c7 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 24 Mar 2024 12:33:57 -0500 Subject: [PATCH 76/81] Update HasOrigin.php --- src/Data/HasOrigin.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 27b80336377..fe045594ee8 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -12,8 +12,6 @@ trait HasOrigin protected $origin; private $cachedKeys; - protected $cachedKeys = null; - public function keys() { if ($this->cachedKeys) { From 274b2b11664e1641ae49f6cf3f46273d9c8d255c Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 24 Mar 2024 13:25:00 -0500 Subject: [PATCH 77/81] Disable for now. --- src/Statamic.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Statamic.php b/src/Statamic.php index eb1f8a77021..6867b4f654d 100644 --- a/src/Statamic.php +++ b/src/Statamic.php @@ -212,7 +212,7 @@ public static function clearApiRouteCache() public static function isApiRoute() { if (self::$isApiRouteCache !== null) { - return self::$isApiRouteCache; + // return self::$isApiRouteCache; } if (! config('statamic.api.enabled') || ! static::pro()) { From 62136fdfaef8e7491db155c046b51095a592d154 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 30 Mar 2024 11:08:01 -0500 Subject: [PATCH 78/81] Update CollectionStructureTest.php --- tests/Data/Structures/CollectionStructureTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Data/Structures/CollectionStructureTest.php b/tests/Data/Structures/CollectionStructureTest.php index 1f635583945..1fec723a26e 100644 --- a/tests/Data/Structures/CollectionStructureTest.php +++ b/tests/Data/Structures/CollectionStructureTest.php @@ -50,11 +50,6 @@ public function queryBuilderPluckReturnValue() return $this->queryBuilderPluckReturnValue ?? collect(); } - public function queryBuilderPluckReturnValue() - { - return $this->queryBuilderPluckReturnValue ?? collect(); - } - /** @test */ public function it_gets_and_sets_the_handle() { From 86623818858e3248d38fa60ab5ced863840ad0c4 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 30 Mar 2024 13:58:09 -0500 Subject: [PATCH 79/81] Add some comments/clarification --- src/Stache/Stores/CollectionTreeStore.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Stache/Stores/CollectionTreeStore.php b/src/Stache/Stores/CollectionTreeStore.php index cd9c7950418..4ee066444d3 100644 --- a/src/Stache/Stores/CollectionTreeStore.php +++ b/src/Stache/Stores/CollectionTreeStore.php @@ -44,6 +44,9 @@ public function save($item) { parent::save($item); + // Ensures indexes are updated. An example + // where this is important is changing + // parent/child tree relationships. Stache::updateDependantIndexes('entries', $item->handle()); } } From f92f021d237d7035a61eb5bc485ceb73bf121ede Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 30 Mar 2024 14:55:49 -0500 Subject: [PATCH 80/81] Refactor to WeakMap to prevent Stache from holding references --- src/Stache/Stache.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index bbacc8bb926..bc8df6375af 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -11,6 +11,7 @@ use Statamic\Support\Str; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; +use WeakMap; use Wilderborn\Partyline\Facade as Partyline; class Stache @@ -77,10 +78,10 @@ public function itemUsingIndexValues($index, $item) $this->registerDependantIndexes($item); if (! array_key_exists($index, $this->indexReferences)) { - $this->indexReferences[$index] = []; + $this->indexReferences[$index] = new WeakMap(); } - $this->indexReferences[$index][] = $item; + $this->indexReferences[$index][$item] = 1; } public function flushIndexValues($index) @@ -89,7 +90,7 @@ public function flushIndexValues($index) return; } - foreach ($this->indexReferences[$index] as $item) { + foreach ($this->indexReferences[$index] as $item => $value) { if (! method_exists($item, 'flushIndexedValue')) { continue; } From d66d2bccfb50a5d4717ed7a65542f094c5a87d43 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 30 Mar 2024 16:14:50 -0500 Subject: [PATCH 81/81] Fallback to empty array if no entry --- src/Stache/Stores/BasicStore.php | 2 +- src/Structures/Page.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stache/Stores/BasicStore.php b/src/Stache/Stores/BasicStore.php index 44b28a7a6b2..c39fe0e2631 100644 --- a/src/Stache/Stores/BasicStore.php +++ b/src/Stache/Stores/BasicStore.php @@ -86,7 +86,7 @@ protected function getCachedItem($key) foreach ($item->receivesIndexValues() as $index) { Stache::itemUsingIndexValues($index, $item); - $value = $this->resolveIndex($index)->get($id); + $value = $this->resolveIndex($index)->load()->get($id); if ($value) { $item->withIndexedValue($index, $value); diff --git a/src/Structures/Page.php b/src/Structures/Page.php index f5a937d8e04..ab90ea0d553 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -425,7 +425,7 @@ public function routeData() return $this->routeData; } - return $this->routeData = $this->entry()->routeData(); + return $this->routeData = $this->entry()?->routeData() ?? []; } public function published()