diff --git a/src/Data/ReceivesIndexValues.php b/src/Data/ReceivesIndexValues.php new file mode 100644 index 00000000000..70718b32ef9 --- /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 f3cfe047696..d7c01716a89 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -26,6 +26,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 +44,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; @@ -55,7 +55,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 HasDirtyState; use HasOrigin { @@ -337,6 +337,8 @@ public function saveQuietly() public function save() { + $this->flushIndexedValues(); + $isNew = is_null(Facades\Entry::find($this->id())); $withEvents = $this->withEvents; @@ -546,6 +548,18 @@ public function date($date = null) ->args(func_get_args()); } + public function receivesIndexValues() + { + return ['uri']; + } + + public function getDependentIndexes() + { + return [ + 'entries' => ['uri'], + ]; + } + public function hasDate() { return $this->collection()->dated(); @@ -861,6 +875,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/Extend/RegistersItself.php b/src/Extend/RegistersItself.php index f96d355b36f..0c1f87f8949 100644 --- a/src/Extend/RegistersItself.php +++ b/src/Extend/RegistersItself.php @@ -2,6 +2,9 @@ namespace Statamic\Extend; +use Statamic\Tags\Tags; +use Statamic\View\Antlers\Language\Analyzers\NodeTypeAnalyzer; + trait RegistersItself { public static function register() @@ -20,5 +23,24 @@ public static function register() return $bindings; }); + + self::updateRegisteredEnvironmentDetails(); + } + + private static function updateRegisteredEnvironmentDetails(): void + { + // This static property will be set when + // ViewServiceProvider is registered. + if (NodeTypeAnalyzer::$environmentDetails == null) { + return; + } + + if (self::class != Tags::class || ! app()->has('statamic.tags')) { + return; + } + + // The static $environmentDetails references a singleton. + // We will keep the registered tag names updated here. + NodeTypeAnalyzer::$environmentDetails->setTagNames(app('statamic.tags')->keys()->all()); } } diff --git a/src/Facades/Stache.php b/src/Facades/Stache.php index 6a321cbd1a9..0c980b2e197 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 updateDependentIndexes($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..13a80f32428 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 $dependentIndexClasses = []; + protected $dependentIndexes = []; public function __construct() { $this->stores = collect(); } + protected function registerDependentIndexes($item) + { + $class = get_class($item); + + if (array_key_exists($class, $this->dependentIndexClasses)) { + return; + } + + // Prevent registering the same class multiple times. + $this->dependentIndexClasses[$class] = true; + + $dependencies = $item->getDependentIndexes(); + + foreach ($dependencies as $store => $indexNames) { + if (! array_key_exists($store, $this->dependentIndexes)) { + $this->dependentIndexes[$store] = []; + } + + $this->dependentIndexes[$store] = array_merge($this->dependentIndexes[$store], $indexNames); + } + } + + public function updateDependentIndexes($store, $handle) + { + if (! array_key_exists($store, $this->dependentIndexes)) { + return; + } + + $this->withoutIndexedValues(function () use ($store, $handle) { + $storeInstance = $this->store($store); + foreach ($this->dependentIndexes[$store] as $index) { + if ($storeInstance instanceof AggregateStore) { + $storeInstance->store($handle)->index($index)->update(); + } else { + $storeInstance->index($index)->update(); + } + } + }); + } + + public function itemUsingIndexValues($index, $item) + { + $this->registerDependentIndexes($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) { 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..572361bd32d 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::updateDependentIndexes('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/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() diff --git a/tests/Antlers/Runtime/EnvironmentDetailsTest.php b/tests/Antlers/Runtime/EnvironmentDetailsTest.php new file mode 100644 index 00000000000..ab004f11be3 --- /dev/null +++ b/tests/Antlers/Runtime/EnvironmentDetailsTest.php @@ -0,0 +1,59 @@ +routes('{slug}')->save(); + EntryFactory::collection('pages')->id('1')->data(['title' => 'The Title', 'content' => 'The content'])->slug('/')->create(); + + (new class extends Tags + { + public static $handle = 'the_tag'; + + public function index() + { + return 'The Tag!'; + } + })::register(); + + (new class extends Modifier + { + protected static $handle = 'the_modifier'; + + public function index($value, $params, $context) + { + return mb_strtoupper($value); + } + })::register(); + + $layout = <<<'LAYOUT' +{{ template_content }} +LAYOUT; + $default = <<<'DEFAULT' +{{ the_tag /}}{{ title | the_modifier /}} +DEFAULT; + + $this->withFakeViews(); + $this->viewShouldReturnRaw('layout', $layout); + $this->viewShouldReturnRaw('default', $default); + + $responseOne = $this->get('/')->assertOk(); + $content = trim($responseOne->content()); + + $this->assertSame('The Tag!THE TITLE', $content); + } +} 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);