Skip to content

Commit e9edd39

Browse files
o1yclaudejasonvarga
authored
[5.x] Fix Stache index re-entrancy causing null URIs on cold stache (#14181)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Jason Varga <jason@pixelfear.com>
1 parent afd636f commit e9edd39

4 files changed

Lines changed: 105 additions & 19 deletions

File tree

src/Stache/Indexes/Index.php

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ abstract class Index
1111
protected $name;
1212
protected $items = [];
1313
protected $loaded = false;
14-
private static ?string $currentlyLoading = null;
14+
private static array $loadingStack = [];
1515

1616
public function __construct($store, $name)
1717
{
@@ -66,27 +66,29 @@ public function load()
6666
}
6767

6868
$loadingKey = $this->store->key().'/'.$this->name;
69-
$currentlyLoadingThis = static::$currentlyLoading === $loadingKey;
69+
$currentlyLoadingThis = in_array($loadingKey, static::$loadingStack);
7070

71-
static::$currentlyLoading = $loadingKey;
71+
static::$loadingStack[] = $loadingKey;
7272

73-
$this->loaded = true;
73+
try {
74+
$this->loaded = true;
7475

75-
if (Statamic::isWorker() && ! $currentlyLoadingThis) {
76-
$this->loaded = false;
77-
}
78-
79-
debugbar()->addMessage("Loading index: {$loadingKey}", 'stache');
76+
if (Statamic::isWorker() && ! $currentlyLoadingThis) {
77+
$this->loaded = false;
78+
}
8079

81-
$this->items = Stache::cacheStore()->get($this->cacheKey());
80+
debugbar()->addMessage("Loading index: {$loadingKey}", 'stache');
8281

83-
if ($this->items === null) {
84-
$this->update();
85-
}
82+
$this->items = Stache::cacheStore()->get($this->cacheKey());
8683

87-
$this->store->cacheIndexUsage($this);
84+
if ($this->items === null) {
85+
$this->update();
86+
}
8887

89-
static::$currentlyLoading = null;
88+
$this->store->cacheIndexUsage($this);
89+
} finally {
90+
array_pop(static::$loadingStack);
91+
}
9092

9193
return $this;
9294
}
@@ -161,8 +163,14 @@ public function clear()
161163
Stache::cacheStore()->forget($this->cacheKey());
162164
}
163165

166+
/** @deprecated */
164167
public static function currentlyLoading()
165168
{
166-
return static::$currentlyLoading;
169+
return end(static::$loadingStack) ?: null;
170+
}
171+
172+
public static function isLoading(): bool
173+
{
174+
return ! empty(static::$loadingStack);
167175
}
168176
}

src/Stache/Stores/CollectionEntriesStore.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,7 @@ protected function getCachedItem($key)
234234
return null;
235235
}
236236

237-
$isLoadingIds = Index::currentlyLoading() === $this->key().'/id';
238-
239-
if (! $isLoadingIds && $this->shouldBlinkEntryUris && ($uri = $this->resolveIndex('uri')->load()->get($entry->id()))) {
237+
if (! Index::isLoading() && $this->shouldBlinkEntryUris && ($uri = $this->resolveIndex('uri')->load()->get($entry->id()))) {
240238
Blink::store('entry-uris')->put($entry->id(), $uri);
241239
}
242240

src/Stache/Stores/Store.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ public function paths()
319319
return $isDuplicate ?? false;
320320
});
321321

322+
$items->each(fn ($item) => $this->cacheItem($item['item']));
323+
322324
$paths = $items->pluck('path', 'key');
323325

324326
$this->cachePaths($paths);

tests/Stache/ColdStacheUriTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Tests\Stache;
4+
5+
use Facades\Tests\Factories\EntryFactory;
6+
use PHPUnit\Framework\Attributes\Test;
7+
use Statamic\Facades\Blink;
8+
use Statamic\Facades\Collection;
9+
use Statamic\Facades\Entry;
10+
use Statamic\Facades\Stache;
11+
use Tests\PreventSavingStacheItemsToDisk;
12+
use Tests\TestCase;
13+
14+
class ColdStacheUriTest extends TestCase
15+
{
16+
use PreventSavingStacheItemsToDisk;
17+
18+
private function simulateColdStache(): void
19+
{
20+
Stache::clear();
21+
Blink::flush();
22+
}
23+
24+
#[Test]
25+
public function entries_have_uris_on_cold_stache_with_single_site_structured_collection()
26+
{
27+
$collection = Collection::make('pages')
28+
->routes('{parent_uri}/{slug}')
29+
->structureContents(['root' => true]);
30+
$collection->save();
31+
32+
EntryFactory::id('alfa-id')->collection('pages')->slug('alfa')->data(['title' => 'Alfa'])->create();
33+
EntryFactory::id('bravo-id')->collection('pages')->slug('bravo')->data(['title' => 'Bravo'])->create();
34+
35+
$this->simulateColdStache();
36+
37+
// Loading a non-URI index first triggers re-entrant URI index loading via getCachedItem().
38+
Stache::store('entries')->store('pages')->index('site')->load();
39+
40+
$entries = Entry::query()
41+
->where('collection', 'pages')
42+
->whereNotNull('uri')
43+
->whereStatus('published')
44+
->get();
45+
46+
$this->assertCount(2, $entries);
47+
}
48+
49+
#[Test]
50+
public function entries_have_uris_on_cold_stache_with_multisite_structured_collection()
51+
{
52+
$this->setSites([
53+
'en' => ['url' => 'http://localhost/', 'locale' => 'en_US'],
54+
'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr_FR'],
55+
]);
56+
57+
$collection = Collection::make('pages')
58+
->routes('{parent_uri}/{slug}')
59+
->structureContents(['root' => true])
60+
->sites(['en', 'fr']);
61+
$collection->save();
62+
63+
EntryFactory::id('alfa-id')->locale('en')->collection('pages')->slug('alfa')->data(['title' => 'Alfa'])->create();
64+
EntryFactory::id('bravo-id')->locale('fr')->collection('pages')->slug('bravo')->origin('alfa-id')->data(['title' => 'Bravo'])->create();
65+
66+
$this->simulateColdStache();
67+
68+
Stache::store('entries')->store('pages')->index('site')->load();
69+
70+
$entries = Entry::query()
71+
->where('collection', 'pages')
72+
->whereNotNull('uri')
73+
->whereStatus('published')
74+
->get();
75+
76+
$this->assertCount(2, $entries);
77+
}
78+
}

0 commit comments

Comments
 (0)