diff --git a/config/stache.php b/config/stache.php index e0db82e05ca..48ede4a4387 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' => false, + 'dump_values' => false, + ], + ]; diff --git a/resources/css/components/items.css b/resources/css/components/items.css index 8940523217c..17069075339 100644 --- a/resources/css/components/items.css +++ b/resources/css/components/items.css @@ -10,7 +10,7 @@ } .item-inner { - @apply w-full flex items-center p-2; + @apply w-full flex items-center px-2; } &.invalid { diff --git a/src/Auth/AugmentedUser.php b/src/Auth/AugmentedUser.php index 657eff38ae3..2968236c3c2 100644 --- a/src/Auth/AugmentedUser.php +++ b/src/Auth/AugmentedUser.php @@ -45,7 +45,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); @@ -63,7 +63,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/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/Contracts/Data/BulkAugmentable.php b/src/Contracts/Data/BulkAugmentable.php new file mode 100644 index 00000000000..add62ea0204 --- /dev/null +++ b/src/Contracts/Data/BulkAugmentable.php @@ -0,0 +1,8 @@ +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())); + $this->isSelecting = true; + foreach ($keys as $key) { - $arr[$key] = $this->get($key); + $arr[$key] = (new TransientValue(null, $key, null))->withAugmentationReferences($this, $fields->get($key)); } + $this->isSelecting = false; + return (new AugmentedCollection($arr))->withRelations($this->relations); } abstract public function keys(); - public function get($handle): Value + public function getAugmentedMethodValue($method) + { + if ($this->methodExistsOnThisClass($method)) { + return $this->$method(); + } + + return $this->data->$method(); + } + + 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)) { - $value = $this->$method(); - - return $value instanceof Value - ? $value - : new Value($value, $method, null, $this->data); + $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->wrapValue($this->data->$method(), $handle); + // 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); + return $value; } protected function filterKeys($keys) @@ -80,7 +109,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); @@ -93,19 +122,48 @@ protected function getFromData($handle) return $value; } - protected function wrapValue($value, $handle) + 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); + + return (new InvokableValue( + null, + $handle, + $fieldtype, + $this->data + ))->setInvokableDetails($method, $proxy, $methodTarget); + } + + 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 blueprintFields() + protected function getFieldtype($handle) + { + return optional($this->blueprintFields()->get($handle))->fieldtype(); + } + + public function blueprintFields() { if (! isset($this->blueprintFields)) { $this->blueprintFields = (method_exists($this->data, 'blueprint') && $blueprint = $this->data->blueprint()) diff --git a/src/Data/AugmentedCollection.php b/src/Data/AugmentedCollection.php index 460300ead98..bcd88e95ff4 100644 --- a/src/Data/AugmentedCollection.php +++ b/src/Data/AugmentedCollection.php @@ -47,6 +47,29 @@ public function withoutEvaluation() return $this; } + protected function requiresMaterialization($item) + { + return $item instanceof InvokableValue || + $item instanceof DeferredValue || + $item instanceof TransientValue; + } + + public function all() + { + return collect($this->items)->map(function ($item) { + if ($this->requiresMaterialization($item)) { + 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/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/BulkAugmentor.php b/src/Data/BulkAugmentor.php new file mode 100644 index 00000000000..97561531f6d --- /dev/null +++ b/src/Data/BulkAugmentor.php @@ -0,0 +1,95 @@ +getAugmentationReferenceKey()) { + return $key; + } + + 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/Data/Concerns/ResolvesValues.php b/src/Data/Concerns/ResolvesValues.php new file mode 100644 index 00000000000..7fead66c110 --- /dev/null +++ b/src/Data/Concerns/ResolvesValues.php @@ -0,0 +1,57 @@ +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(); + } + + public function isRelationship(): bool + { + $this->resolve(); + + return parent::isRelationship(); + } + + public function fieldtype() + { + $this->resolve(); + + return parent::fieldtype(); + } +} diff --git a/src/Data/DeferredValue.php b/src/Data/DeferredValue.php new file mode 100644 index 00000000000..aac8b403995 --- /dev/null +++ b/src/Data/DeferredValue.php @@ -0,0 +1,36 @@ +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; + } +} diff --git a/src/Data/HasAugmentedInstance.php b/src/Data/HasAugmentedInstance.php index 635f54f9279..b8221b1ecd1 100644 --- a/src/Data/HasAugmentedInstance.php +++ b/src/Data/HasAugmentedInstance.php @@ -17,11 +17,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) @@ -29,6 +29,11 @@ public function toAugmentedArray($keys = null) return $this->toAugmentedCollection($keys)->all(); } + public function toDeferredAugmentedArray($keys = null, $fields = null) + { + return $this->toAugmentedCollection($keys, $fields)->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..7c3bd2adbcd --- /dev/null +++ b/src/Data/InvokableValue.php @@ -0,0 +1,80 @@ +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(); + } +} 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/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(); + } +} diff --git a/src/Entries/Collection.php b/src/Entries/Collection.php index 7a6e3a89272..db002575f2f 100644 --- a/src/Entries/Collection.php +++ b/src/Entries/Collection.php @@ -99,6 +99,8 @@ public function routes($routes = null) return [$site => $siteRoute]; }); + })->afterSetter(function () { + $this->cachedRoutes = null; }) ->afterSetter(fn () => $this->cachedRoutes = null) ->args(func_get_args()); @@ -474,6 +476,7 @@ public function save() Facades\Collection::save($this); Blink::forget('collection-handles'); + Blink::forget('mounted-collections'); Blink::flushStartingWith("collection-{$this->id()}"); if ($isNew) { @@ -766,6 +769,8 @@ public function delete() CollectionDeleted::dispatch($this); + Blink::forget('mounted-collections'); + return true; } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 1be9722dc9e..aa49067314a 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; @@ -26,6 +27,7 @@ use Statamic\Data\HasDirtyState; use Statamic\Data\HasOrigin; use Statamic\Data\Publishable; +use Statamic\Data\ReceivesIndexValues; use Statamic\Data\TracksLastModified; use Statamic\Data\TracksQueriedColumns; use Statamic\Data\TracksQueriedRelations; @@ -43,7 +45,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; @@ -53,9 +54,9 @@ 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; + use ContainsComputedData, ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, Localizable, Publishable, ReceivesIndexValues, Revisable, Searchable, TracksLastModified, TracksQueriedColumns, TracksQueriedRelations; use HasDirtyState; use HasOrigin { @@ -78,6 +79,11 @@ class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableVal protected $withEvents = true; protected $template; protected $layout; + protected $augmentationReferenceKey; + protected $computedCallbackCache; + protected $hasDate; + protected $hasTime; + protected $hasSeconds; private $siteCache; public function __construct() @@ -91,6 +97,17 @@ public function id($id = null) return $this->fluentlyGetOrSet('id')->args(func_get_args()); } + public function getAugmentationReferenceKey(): ?string + { + if ($this->augmentationReferenceKey) { + return $this->augmentationReferenceKey; + } + + $dataPart = implode('|', $this->data->keys()->sort()->all()); + + return $this->augmentationReferenceKey = 'Entry::'.$this->blueprint()->namespace().'::'.$dataPart; + } + public function locale($locale = null) { return $this @@ -128,6 +145,8 @@ public function collection($collection = null) }) : null; } + $this->computedCallbackCache = null; + $this->clearDateTimePropertyCaches(); $this->collection = $collection instanceof \Statamic\Contracts\Entries\Collection ? $collection->handle() : $collection; return $this; @@ -163,6 +182,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; @@ -335,6 +356,8 @@ public function saveQuietly() public function save() { + $this->flushIndexedValues(); + $isNew = is_null(Facades\Entry::find($this->id())); $withEvents = $this->withEvents; @@ -544,27 +567,58 @@ public function date($date = null) ->args(func_get_args()); } + public function receivesIndexValues() + { + return ['uri']; + } + + public function getDependantIndexes() + { + return [ + 'entries' => ['uri'], + ]; + } + + 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() @@ -859,6 +913,12 @@ public function routeData() public function uri() { + $indexedUri = $this->getIndexedValue('uri'); + + if ($indexedUri !== null) { + return $indexedUri; + } + if (! $this->route()) { return null; } @@ -1019,16 +1079,27 @@ 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 __sleep() + public function __serialize(): array { if ($this->slug instanceof Closure) { $slug = $this->slug; $this->slug = $slug($this); } - return array_keys(get_object_vars($this)); + return Arr::except(get_object_vars($this), ['computedCallbackCache']); + } + + public function __unserialize(array $data): void + { + foreach ($data as $key => $value) { + $this->{$key} = $value; + } } } diff --git a/src/Facades/Stache.php b/src/Facades/Stache.php index 6a321cbd1a9..1eb96b40f4c 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() @@ -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/Fields/Blueprint.php b/src/Fields/Blueprint.php index e1278480f96..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,6 +43,8 @@ class Blueprint implements Arrayable, ArrayAccess, Augmentable, QueryableValue protected $ensuredFields = []; protected $afterSaveCallbacks = []; protected $withEvents = true; + protected $lastBlueprintHandle = null; + private ?Columns $columns = null; public function setHandle(string $handle) @@ -623,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/Fields/Field.php b/src/Fields/Field.php index 5c48e79707f..d1a900d3863 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\Rules\Handle; use Statamic\Support\Arr; @@ -102,7 +103,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/src/Fields/Value.php b/src/Fields/Value.php index 0ddd7b024bf..c1be6912d3d 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/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index 4f0b538afe5..9b171b24203 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); } /** @@ -767,7 +768,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; @@ -838,7 +839,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); } @@ -2057,7 +2058,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/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/Query/Builder.php b/src/Query/Builder.php index 5a013e3b1ce..5da373f67a2 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/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/Query/Builder.php b/src/Stache/Query/Builder.php index e79fa7a6017..8baa3c7cc08 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; @@ -14,6 +15,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() @@ -37,13 +40,21 @@ public function pluck($column, $key = null) public function get($columns = ['*']) { + $startTime = hrtime(true); + $items = $this->getItems($this->resolveKeys()); $items->each(fn ($item) => $item ->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..c15432cea72 --- /dev/null +++ b/src/Stache/Query/Concerns/LogsStacheQueries.php @@ -0,0 +1,51 @@ +collections)) { + $extraFrom = implode(', ', $this->collections); + } + } + + return (new StacheQueryDumper( + $this->store, + $this->wheres, + $this->columns, + $this->orderBys, + $this->limit, + $this->offset + )) + ->setDumpActualValues($this->logRealValues) + ->setExtraFromStatement($extraFrom) + ->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..220af3fc36c --- /dev/null +++ b/src/Stache/Query/StacheQueryDumper.php @@ -0,0 +1,61 @@ +store = $store; + $this->wheres = $wheres; + $this->columns = $columns; + $this->orderBys = $orderBys; + $this->limit = $limit; + $this->offset = $offset; + } + + public function setExtraFromStatement($extraFrom): self + { + $this->extraFrom = $extraFrom; + + return $this; + } + + public function setDumpActualValues($dumpValues): self + { + $this->dumpActualValues = $dumpValues; + + return $this; + } + + 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(); + + return $query; + } +} diff --git a/src/Stache/Repositories/CollectionRepository.php b/src/Stache/Repositories/CollectionRepository.php index b57c7412e30..66d2e025937 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/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/Stache.php b/src/Stache/Stache.php index 1bfab301e9f..dec3a515e58 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -6,10 +6,12 @@ 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; use Symfony\Component\Lock\LockInterface; +use WeakMap; use Wilderborn\Partyline\Facade as Partyline; class Stache @@ -21,12 +23,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] = new WeakMap(); + } + + $this->indexReferences[$index][$item] = 1; + } + + public function flushIndexValues($index) + { + if (! array_key_exists($index, $this->indexReferences)) { + return; + } + + foreach ($this->indexReferences[$index] as $item => $value) { + 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) { @@ -98,7 +195,7 @@ public function refresh() return $this->clear()->warm(); } - public function warm() + public function warm($stores = []) { Partyline::comment('Warming Stache...'); @@ -106,7 +203,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(); diff --git a/src/Stache/Stores/AggregateStore.php b/src/Stache/Stores/AggregateStore.php index 334c0b63835..40330377f92 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/BasicStore.php b/src/Stache/Stores/BasicStore.php index 96c9926b069..c39fe0e2631 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 @@ -77,7 +78,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)->load()->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..4ee066444d3 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,14 @@ protected function newTreeClassByPath($path) ->locale($site) ->handle($handle); } + + 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()); + } } 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/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', + ]; + } } diff --git a/src/Stache/Stores/Store.php b/src/Stache/Stores/Store.php index 65785353faf..ff1f422c46b 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/src/Statamic.php b/src/Statamic.php index b99e7d210be..6867b4f654d 100644 --- a/src/Statamic.php +++ b/src/Statamic.php @@ -37,6 +37,7 @@ class Statamic protected static $jsonVariables = []; protected static $bootedCallbacks = []; protected static $afterInstalledCallbacks = []; + protected static $isApiRouteCache; public static function version() { @@ -203,13 +204,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/Structures/AugmentedPage.php b/src/Structures/AugmentedPage.php index e2cd7f0286f..398eec01b4f 100644 --- a/src/Structures/AugmentedPage.php +++ b/src/Structures/AugmentedPage.php @@ -52,7 +52,7 @@ private function apiKeys($keys) }); } - protected function getFromData($key) + public function getFromData($key) { if ($key === 'title') { return $this->page->title(); @@ -61,7 +61,7 @@ protected function getFromData($key) return $this->page->getSupplement($key) ?? $this->page->value($key); } - protected function blueprintFields() + public function blueprintFields() { if ($this->fieldsCache) { return $this->fieldsCache; diff --git a/src/Structures/Nav.php b/src/Structures/Nav.php index da840d64adf..e69be514df5 100644 --- a/src/Structures/Nav.php +++ b/src/Structures/Nav.php @@ -137,6 +137,6 @@ public function blueprint() NavBlueprintFound::dispatch($blueprint, $this); - return $blueprint; + return $this->blueprintCache = $blueprint; } } diff --git a/src/Structures/Page.php b/src/Structures/Page.php index f5a937d8e04..8a8cdf07c82 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,6 +40,8 @@ class Page implements Arrayable, ArrayAccess, Augmentable, Entry, JsonSerializab protected $title; protected $depth; protected $data = []; + protected $augmentationReferenceKey; + protected $setAugmentationReferenceKey = false; private $absoluteUrl; private $absoluteUrlWithoutRedirect; private $blueprint; @@ -52,6 +55,21 @@ public function __construct() $this->supplements = collect(); } + public function getAugmentationReferenceKey(): ?string + { + if ($this->setAugmentationReferenceKey) { + return $this->augmentationReferenceKey; + } + + $this->setAugmentationReferenceKey = true; + + if ($entry = $this->entry()) { + return $this->augmentationReferenceKey = 'Page::'.$entry->getAugmentationReferenceKey(); + } + + return $this->augmentationReferenceKey = 'Page::'; + } + public function setUrl($url) { $this->url = $url; @@ -425,7 +443,7 @@ public function routeData() return $this->routeData; } - return $this->routeData = $this->entry()->routeData(); + return $this->routeData = $this->entry()?->routeData() ?? []; } public function published() 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; + } +} 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/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/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', 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); + } +} 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)); } } diff --git a/src/Tags/Structure.php b/src/Tags/Structure.php index bdaecf27080..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->toAugmentedArray($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(); 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..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->toAugmentedArray(); + 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 { diff --git a/src/View/Cascade.php b/src/View/Cascade.php index 4c8d2c32b9e..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); } } @@ -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) { 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(); 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/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(); diff --git a/tests/Data/AugmentedTest.php b/tests/Data/AugmentedTest.php index fc5a709b193..682bcdfb011 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 */ @@ -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/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); diff --git a/tests/Data/Entries/CollectionTest.php b/tests/Data/Entries/CollectionTest.php index 9d875bc1eb7..728220f0630 100644 --- a/tests/Data/Entries/CollectionTest.php +++ b/tests/Data/Entries/CollectionTest.php @@ -500,6 +500,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(); diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index 54a060c8505..e745a686d2f 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; 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/Data/HasAugmentedInstanceTest.php b/tests/Data/HasAugmentedInstanceTest.php index 51bca9e13ea..6d7f56ea39b 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) @@ -98,7 +98,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) { @@ -130,7 +130,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) { diff --git a/tests/Data/Structures/CollectionStructureTest.php b/tests/Data/Structures/CollectionStructureTest.php index 13479040892..1fec723a26e 100644 --- a/tests/Data/Structures/CollectionStructureTest.php +++ b/tests/Data/Structures/CollectionStructureTest.php @@ -30,6 +30,9 @@ public function setUp(): void $this->entryQueryBuilder->shouldReceive('pluck')->andReturnUsing(function () { return $this->queryBuilderPluckReturnValue(); }); + $this->entryQueryBuilder->shouldReceive('pluck')->andReturnUsing(function () { + return $this->queryBuilderPluckReturnValue(); + }); $this->collection = $this->mock(Collection::class); $this->collection->shouldReceive('queryEntries')->andReturn($this->entryQueryBuilder); @@ -84,6 +87,10 @@ public function it_makes_a_tree() 1, ]); + $this->queryBuilderPluckReturnValue = collect([ + 1, + ]); + $tree = $structure->makeTree('fr', [ ['entry' => 1], ]); @@ -261,6 +268,11 @@ public function the_tree_root_can_have_children_when_not_expecting_root() 456, ]); + $this->queryBuilderPluckReturnValue = collect([ + 123, + 456, + ]); + parent::the_tree_root_can_have_children_when_not_expecting_root(); } @@ -274,6 +286,11 @@ public function only_entries_belonging_to_the_associated_collection_may_be_in_th 2, ]); + $this->queryBuilderPluckReturnValue = collect([ + 1, + 2, + ]); + $validated = $this->structure('test')->validateTree([ [ 'entry' => '1', @@ -308,6 +325,14 @@ public function entries_not_explicitly_in_the_tree_should_be_appended_to_the_end 5, ]); + $this->queryBuilderPluckReturnValue = collect([ + 1, + 2, + 3, + 4, + 5, + ]); + $actual = $this->structure('test')->validateTree([ [ 'entry' => '1', 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()); 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);