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
13 changes: 9 additions & 4 deletions src/Laravel/Metadata/CachePropertyMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,24 @@
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use Illuminate\Support\Facades\Cache;

final readonly class CachePropertyMetadataFactory implements PropertyMetadataFactoryInterface
final class CachePropertyMetadataFactory implements PropertyMetadataFactoryInterface
{
/**
* @var array<string, ApiProperty>
*/
private array $localCache = [];

public function __construct(
private PropertyMetadataFactoryInterface $decorated,
private string $cacheStore,
private readonly PropertyMetadataFactoryInterface $decorated,
private readonly string $cacheStore,
) {
}

public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
$key = hash('xxh3', serialize(['resource_class' => $resourceClass, 'property' => $property] + $options));

return Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $property, $options) {
return $this->localCache[$key] ??= Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $property, $options) {
return $this->decorated->create($resourceClass, $property, $options);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,24 @@
use ApiPlatform\Metadata\Property\PropertyNameCollection;
use Illuminate\Support\Facades\Cache;

final readonly class CachePropertyNameCollectionMetadataFactory implements PropertyNameCollectionFactoryInterface
final class CachePropertyNameCollectionMetadataFactory implements PropertyNameCollectionFactoryInterface
{
/**
* @var array<string, PropertyNameCollection>
*/
private array $localCache = [];

public function __construct(
private PropertyNameCollectionFactoryInterface $decorated,
private string $cacheStore,
private readonly PropertyNameCollectionFactoryInterface $decorated,
private readonly string $cacheStore,
) {
}

public function create(string $resourceClass, array $options = []): PropertyNameCollection
{
$key = hash('xxh3', serialize(['resource_class' => $resourceClass] + $options));

return Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $options) {
return $this->localCache[$key] ??= Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $options) {
return $this->decorated->create($resourceClass, $options);
});
}
Expand Down
13 changes: 9 additions & 4 deletions src/Laravel/Metadata/CacheResourceCollectionMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use Illuminate\Support\Facades\Cache;

final readonly class CacheResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface
final class CacheResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface
{
/**
* @var array<string, ResourceMetadataCollection>
*/
private array $localCache = [];

public function __construct(
private ResourceMetadataCollectionFactoryInterface $decorated,
private string $cacheStore,
private readonly ResourceMetadataCollectionFactoryInterface $decorated,
private readonly string $cacheStore,
) {
}

public function create(string $resourceClass): ResourceMetadataCollection
{
return Cache::store($this->cacheStore)->rememberForever($resourceClass, function () use ($resourceClass) {
return $this->localCache[$resourceClass] ??= Cache::store($this->cacheStore)->rememberForever($resourceClass, function () use ($resourceClass) {
return $this->decorated->create($resourceClass);
});
}
Expand Down
129 changes: 129 additions & 0 deletions src/Laravel/Tests/Metadata/CacheMetadataFactoriesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\Tests\Metadata;

use ApiPlatform\Laravel\Metadata\CachePropertyMetadataFactory;
use ApiPlatform\Laravel\Metadata\CachePropertyNameCollectionMetadataFactory;
use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Property\PropertyNameCollection;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use Illuminate\Support\Facades\Cache;
use Orchestra\Testbench\TestCase;

/**
* Tests for issue #8413.
*
* Ensures the metadata cache factories serve repeated lookups from an in-memory cache
* instead of consulting the Laravel cache store on every call. Wiping the persistent
* store between two identical lookups proves the second one is answered from memory:
* without the in-memory cache, it would fall through to the (now empty) store and hit
* the decorated factory again.
*/
class CacheMetadataFactoriesTest extends TestCase
{
public function testPropertyMetadataIsMemoizedInMemory(): void
{
$property = new ApiProperty();
$decorated = $this->createMock(PropertyMetadataFactoryInterface::class);
$decorated->expects($this->once())
->method('create')
->with('App\Models\Book', 'title', [])
->willReturn($property);

$factory = new CachePropertyMetadataFactory($decorated, 'array');

$this->assertSame($property, $factory->create('App\Models\Book', 'title'));

Cache::store('array')->clear();

$this->assertSame($property, $factory->create('App\Models\Book', 'title'));
}

public function testPropertyNameCollectionIsMemoizedInMemory(): void
{
$collection = new PropertyNameCollection(['title']);
$decorated = $this->createMock(PropertyNameCollectionFactoryInterface::class);
$decorated->expects($this->once())
->method('create')
->with('App\Models\Book', [])
->willReturn($collection);

$factory = new CachePropertyNameCollectionMetadataFactory($decorated, 'array');

$this->assertSame($collection, $factory->create('App\Models\Book'));

Cache::store('array')->clear();

$this->assertSame($collection, $factory->create('App\Models\Book'));
}

public function testResourceMetadataCollectionIsMemoizedInMemory(): void
{
$collection = new ResourceMetadataCollection('App\Models\Book');
$decorated = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
$decorated->expects($this->once())
->method('create')
->with('App\Models\Book')
->willReturn($collection);

$factory = new CacheResourceCollectionMetadataFactory($decorated, 'array');

$this->assertSame($collection, $factory->create('App\Models\Book'));

Cache::store('array')->clear();

$this->assertSame($collection, $factory->create('App\Models\Book'));
}

public function testDifferentKeysAreCachedIndependently(): void
{
$title = new ApiProperty(description: 'title');
$isbn = new ApiProperty(description: 'isbn');
$decorated = $this->createMock(PropertyMetadataFactoryInterface::class);
$decorated->expects($this->exactly(2))
->method('create')
->willReturnCallback(static fn (string $resourceClass, string $property) => match ($property) {
'title' => $title,
'isbn' => $isbn,
default => throw new \LogicException(\sprintf('Unexpected property "%s".', $property)),
});

$factory = new CachePropertyMetadataFactory($decorated, 'array');

$this->assertSame($title, $factory->create('App\Models\Book', 'title'));
$this->assertSame($isbn, $factory->create('App\Models\Book', 'isbn'));
$this->assertSame($title, $factory->create('App\Models\Book', 'title'));
$this->assertSame($isbn, $factory->create('App\Models\Book', 'isbn'));
}

public function testMetadataIsStoredInThePersistentCacheStore(): void
{
$property = new ApiProperty();
$decorated = $this->createMock(PropertyMetadataFactoryInterface::class);
$decorated->expects($this->once())
->method('create')
->with('App\Models\Book', 'title', [])
->willReturn($property);

$factory = new CachePropertyMetadataFactory($decorated, 'array');
$factory->create('App\Models\Book', 'title');

$key = hash('xxh3', serialize(['resource_class' => 'App\Models\Book', 'property' => 'title']));
$this->assertSame($property, Cache::store('array')->get($key));
}
}
Loading