Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions src/Stache/Stores/CollectionEntriesStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Statamic\Stache\Stores;

use Closure;
use Illuminate\Support\Carbon;
use Statamic\Entries\Entry as EntryInstance;
use Statamic\Entries\GetDateFromPath;
use Statamic\Entries\GetSlugFromPath;
use Statamic\Entries\GetSuffixFromPath;
Expand Down Expand Up @@ -231,9 +233,7 @@ protected function writeItemToDisk($item)

protected function getCachedItem($key)
{
$cacheKey = $this->getItemCacheKey($key);

if (! $entry = Stache::cacheStore()->get($cacheKey)) {
if (! $entry = $this->getEntryLoadedInThisRequest($key) ?? Stache::cacheStore()->get($this->getItemCacheKey($key))) {
return null;
}

Expand All @@ -244,6 +244,32 @@ protected function getCachedItem($key)
return $entry;
}

/**
* While indexes are being built, every entry made from its file in this request is
* still in memory (makeItemFromFile() blinks it). Hand out a shallow clone of it
* rather than unserialising a second full copy from the cache: callers still get
* their own object, and its data is only copied if they change it.
*/
private function getEntryLoadedInThisRequest($key): ?EntryInstance
{
if (! Index::isLoading()) {
return null;
}

if (! ($loaded = Blink::store('structure-entries')->get($key)) instanceof EntryInstance) {
return null;
}

$entry = clone $loaded;

// A copy read from the cache doesn't carry these (see Entry::__sleep).
Closure::bind(function () {
$this->cachedKeys = $this->computedCallbackCache = $this->siteCache = $this->augmentationReferenceKey = null;
}, $entry, EntryInstance::class)();

return $entry;
}

public function withoutBlinkingEntryUris($callback)
{
$this->shouldBlinkEntryUris = false;
Expand Down
4 changes: 3 additions & 1 deletion tests/Data/Entries/EntryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1562,10 +1562,12 @@ public function it_clears_blink_caches_when_saving()
$mock->shouldReceive('store')->with('structure-uris')->once()->andReturn(
$this->mock(\Spatie\Blink\Blink::class)->shouldReceive('forget')->with('a')->once()->getMock()
);
$mock->shouldReceive('store')->with('structure-entries')->twice()->andReturn(
$mock->shouldReceive('store')->with('structure-entries')->atLeast()->twice()->andReturn(
tap($this->mock(\Spatie\Blink\Blink::class), function ($m) {
$m->shouldReceive('forget')->with('a')->once();
$m->shouldReceive('put')->once();
// Indexes loaded during the save may look for entries already in memory.
$m->shouldReceive('get')->andReturnNull();
})
);

Expand Down
67 changes: 67 additions & 0 deletions tests/Stache/Stores/EntriesStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use ReflectionProperty;
use Statamic\Contracts\Entries\Entry;
use Statamic\Facades;
use Statamic\Facades\Blink;
use Statamic\Facades\Path;
use Statamic\Facades\Stache;
use Statamic\Stache\Indexes\Index;
use Statamic\Stache\Stores\EntriesStore;
use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;
Expand Down Expand Up @@ -361,4 +364,68 @@ public function it_ignores_entries_in_a_site_subdirectory_where_the_collection_d
{
$this->markTestIncomplete();
}

#[Test]
public function while_an_index_loads_it_hands_out_a_clone_of_the_entry_built_in_this_request_instead_of_the_cached_copy()
{
$store = $this->parent->store('blog');
$store->paths();

$inMemory = Blink::store('structure-entries')->get('blog-christmas');
$this->putInItemCache($store, 'blog-christmas', (clone $inMemory)->set('title', 'From the cache'));

$item = $this->whileAnIndexLoads(fn () => $store->getItem('blog-christmas'));

$this->assertNotSame($inMemory, $item);
$this->assertEquals('Christmas', $item->get('title'));
$this->assertEquals($inMemory->data()->all(), $item->data()->all());
}

#[Test]
public function changing_the_clone_does_not_change_the_entry_in_memory()
{
$store = $this->parent->store('blog');
$store->paths();

$inMemory = Blink::store('structure-entries')->get('blog-christmas');

$item = $this->whileAnIndexLoads(fn () => $store->getItem('blog-christmas'));
$item->set('title', 'Changed');

$this->assertEquals('Christmas', $inMemory->get('title'));
}

#[Test]
public function outside_of_index_loading_it_still_reads_the_cached_copy()
{
$store = $this->parent->store('blog');
$store->paths();

$inMemory = Blink::store('structure-entries')->get('blog-christmas');
$this->putInItemCache($store, 'blog-christmas', (clone $inMemory)->set('title', 'From the cache'));

$item = $store->getItem('blog-christmas');

$this->assertNotSame($inMemory, $item);
$this->assertEquals('From the cache', $item->get('title'));
}

private function putInItemCache($store, $key, $item)
{
$cacheKey = (fn () => $this->getItemCacheKey($key))->call($store);

Stache::cacheStore()->forever($cacheKey, $item);
}

private function whileAnIndexLoads(callable $callback)
{
$stack = new ReflectionProperty(Index::class, 'loadingStack');
$stack->setValue(null, ['entries::blog/test']);

try {
return $callback();
} finally {
$stack->setValue(null, []);
}
}
}
Loading