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
25 changes: 23 additions & 2 deletions src/Stache/Stores/AggregateStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,30 @@ public function childDirectory($child)

public function getItems($keys)
{
return collect($keys)->map(function ($key) {
return $this->getItem($key);
$keys = collect($keys);

if ($keys->isEmpty()) {
return collect();
}

// Group keys by child store for batch fetching
$grouped = $keys->mapWithKeys(function ($key) {
[$store, $id] = explode('::', $key, 2);

return [$key => compact('store', 'id')];
})->groupBy('store');

// Batch fetch from each child store
$fetched = $grouped->flatMap(function ($items, $store) {
$ids = $items->pluck('id');
$storeItems = $this->store($store)->getItems($ids);

// Re-key with full keys (store::id)
return $ids->mapWithKeys(fn ($id, $i) => ["{$store}::{$id}" => $storeItems[$i]]);
});

// Return in original order
return $keys->map(fn ($key) => $fetched[$key]);
}

public function getItem($key)
Expand Down
53 changes: 53 additions & 0 deletions src/Stache/Stores/BasicStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,59 @@ public function getItemFilter(SplFileInfo $file)

abstract public function makeItemFromFile($path, $contents);

public function getItems($keys)
{
$this->handleFileChanges();

$keys = collect($keys);

if ($keys->isEmpty()) {
return collect();
}

// Only use batch fetch for cache drivers that benefit from it (network-based)
if ($this->shouldUseBatchCaching()) {
return $this->getItemsBatched($keys);
}

return $keys->map(fn ($key) => $this->getItem($key));
}

protected function getItemsBatched($keys)
{
// Build a map of cache keys to item keys
$cacheKeyMap = $keys->mapWithKeys(fn ($key) => [$this->getItemCacheKey($key) => $key]);

// Batch fetch from cache
$cached = Stache::cacheStore()->many($cacheKeyMap->keys()->all());

// Process results, fetching any misses from disk
return $keys->map(function ($key) use ($cached) {
$cacheKey = $this->getItemCacheKey($key);

if ($item = $cached[$cacheKey] ?? null) {
if (method_exists($item, 'syncOriginal')) {
$item->syncOriginal();
}

return $item;
}

// Cache miss - fetch individually (will also cache it)
return $this->getItem($key);
});
}

protected function shouldUseBatchCaching(): bool
{
$store = Stache::cacheStore()->getStore();

// These drivers benefit from batch operations (network round-trip reduction)
return $store instanceof \Illuminate\Cache\RedisStore
|| $store instanceof \Illuminate\Cache\MemcachedStore
|| $store instanceof \Illuminate\Cache\DynamoDbStore;
}

public function getItem($key)
{
$this->handleFileChanges();
Expand Down
64 changes: 64 additions & 0 deletions tests/Stache/AggregateStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,54 @@ public function it_gets_and_sets_child_stores()
$this->assertInstanceOf(ChildStore::class, $childOne);
$this->assertEquals(['one' => $childOne, 'two' => $childTwo], $this->store->stores()->all());
}

#[Test]
public function it_gets_items_from_multiple_child_stores()
{
$this->store->store('one')->setItems([
'a' => ['id' => 'a', 'title' => 'Item A'],
'b' => ['id' => 'b', 'title' => 'Item B'],
]);

$this->store->store('two')->setItems([
'c' => ['id' => 'c', 'title' => 'Item C'],
'd' => ['id' => 'd', 'title' => 'Item D'],
]);

$items = $this->store->getItems(['one::a', 'two::c', 'one::b']);

$this->assertCount(3, $items);
$this->assertEquals('a', $items[0]['id']);
$this->assertEquals('c', $items[1]['id']);
$this->assertEquals('b', $items[2]['id']);
}

#[Test]
public function it_gets_items_preserving_order_across_stores()
{
$this->store->store('one')->setItems([
'a' => ['id' => 'a'],
'b' => ['id' => 'b'],
]);

$this->store->store('two')->setItems([
'c' => ['id' => 'c'],
'd' => ['id' => 'd'],
]);

// Request in mixed order
$items = $this->store->getItems(['two::d', 'one::a', 'two::c', 'one::b']);

$this->assertEquals(['d', 'a', 'c', 'b'], $items->pluck('id')->all());
}

#[Test]
public function it_returns_empty_collection_for_empty_keys()
{
$items = $this->store->getItems([]);

$this->assertCount(0, $items);
}
}

class TestAggregateStore extends AggregateStore
Expand All @@ -53,4 +101,20 @@ public function discoverStores()

class TestChildStore extends ChildStore
{
protected $items = [];

public function setItems(array $items)
{
$this->items = $items;
}

public function getItem($key)
{
return $this->items[$key] ?? null;
}

public function getItems($keys)
{
return collect($keys)->map(fn ($key) => $this->getItem($key));
}
}
39 changes: 39 additions & 0 deletions tests/Stache/BasicStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,45 @@ public function items_are_different_instances_every_time()
$this->assertNotSame($one, $two);
}

#[Test]
public function it_gets_multiple_items_by_keys()
{
file_put_contents($this->tempDir.'/foo.yaml', '');
file_put_contents($this->tempDir.'/bar.yaml', '');
file_put_contents($this->tempDir.'/baz.yaml', '');

$items = $this->store->getItems(['foo', 'bar', 'baz']);

$this->assertCount(3, $items);
$this->assertEquals('foo', $items[0]->id());
$this->assertEquals('bar', $items[1]->id());
$this->assertEquals('baz', $items[2]->id());
}

#[Test]
public function it_gets_multiple_items_preserving_order()
{
file_put_contents($this->tempDir.'/foo.yaml', '');
file_put_contents($this->tempDir.'/bar.yaml', '');
file_put_contents($this->tempDir.'/baz.yaml', '');

// Request in different order than created
$items = $this->store->getItems(['baz', 'foo', 'bar']);

$this->assertCount(3, $items);
$this->assertEquals('baz', $items[0]->id());
$this->assertEquals('foo', $items[1]->id());
$this->assertEquals('bar', $items[2]->id());
}

#[Test]
public function it_returns_empty_collection_for_empty_keys()
{
$items = $this->store->getItems([]);

$this->assertCount(0, $items);
}

#[Test]
public function it_gets_an_item_by_path()
{
Expand Down