diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 84f2991..09f4a82 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -98,6 +98,15 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('metadata') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('cache') + ->info('Whether or not to cache the metadata') + ->defaultValue(!'%kernel.debug%') + ->end() + ->end() + ->end() ->arrayNode('search') ->canBeEnabled() ->info('Configures your site search (and autocomplete) experience') diff --git a/src/DependencyInjection/SetonoSyliusMeilisearchExtension.php b/src/DependencyInjection/SetonoSyliusMeilisearchExtension.php index 88472eb..a16be4b 100644 --- a/src/DependencyInjection/SetonoSyliusMeilisearchExtension.php +++ b/src/DependencyInjection/SetonoSyliusMeilisearchExtension.php @@ -9,6 +9,9 @@ use Setono\SyliusMeilisearchPlugin\DataMapper\DataMapperInterface; use Setono\SyliusMeilisearchPlugin\DataProvider\IndexableDataProviderInterface; use Setono\SyliusMeilisearchPlugin\Document\Document; +use Setono\SyliusMeilisearchPlugin\Document\Metadata\CachedMetadataFactory; +use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactory; +use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface; use Setono\SyliusMeilisearchPlugin\EventSubscriber\IndexableDataFilter\ChannelsAwareFilter; use Setono\SyliusMeilisearchPlugin\EventSubscriber\IndexableDataFilter\EnabledFilter; use Setono\SyliusMeilisearchPlugin\EventSubscriber\IndexableDataFilter\StockAvailableFilter; @@ -44,6 +47,7 @@ public function load(array $configs, ContainerBuilder $container): void * @var array{ * indexes: array, entities: list, data_provider: class-string, indexer: class-string|null, prefix: string|null, default_filters: array}>, * server: array{ host: string, master_key: string, search_key: string }, + * metadata: array{ cache: bool }, * search: array{ enabled: bool, path: string, index: string, hits_per_page: int }, * autocomplete: array{ enabled: bool, indexes: list, container: string, placeholder: string }, * resources: array, @@ -59,6 +63,13 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('setono_sylius_meilisearch.server.master_key', $config['server']['master_key']); $container->setParameter('setono_sylius_meilisearch.server.search_key', $config['server']['search_key']); + // cache + $metadataCacheEnabled = $config['metadata']['cache']; + $container->setParameter('setono_sylius_meilisearch.cache', $metadataCacheEnabled); + if ($metadataCacheEnabled) { + $this->registerCachedMetadataFactory($container); + } + $loader->load('services.xml'); // auto configuration @@ -450,4 +461,18 @@ private static function registerStockAvailableFilter(ContainerBuilder $container ->addTag('kernel.event_subscriber') ; } + + private function registerCachedMetadataFactory(ContainerBuilder $container): void + { + $container + ->register(CachedMetadataFactory::class) + ->setDecoratedService(MetadataFactory::class) + ->setArguments([ + new Reference(CachedMetadataFactory::class . '.inner'), + new Reference('setono_sylius_meilisearch.cache.metadata'), + ]) + ; + + $container->setAlias(MetadataFactoryInterface::class, CachedMetadataFactory::class); + } } diff --git a/src/Document/Metadata/CachedMetadataFactory.php b/src/Document/Metadata/CachedMetadataFactory.php new file mode 100644 index 0000000..91c2931 --- /dev/null +++ b/src/Document/Metadata/CachedMetadataFactory.php @@ -0,0 +1,62 @@ +, MetadataInterface> + */ + private array $loadedClasses = []; + + public function __construct( + private readonly MetadataFactoryInterface $baseMetadataFactory, + private readonly CacheItemPoolInterface $cache, + ) { + } + + public function getMetadataFor(string|Document $document): MetadataInterface + { + if ($document instanceof Document) { + $document = $document::class; + } + + if (isset($this->loadedClasses[$document])) { + return $this->loadedClasses[$document]; + } + + $cacheItem = $this->cache->getItem($this->escapeClassName($document)); + if ($cacheItem->isHit()) { + $metadata = $cacheItem->get(); + Assert::isInstanceOf($metadata, MetadataInterface::class); + + $this->loadedClasses[$document] = $metadata; + + return $this->loadedClasses[$document]; + } + + $metadata = $this->baseMetadataFactory->getMetadataFor($document); + + $this->cache->save($cacheItem->set($metadata)); + + return $this->loadedClasses[$document] = $metadata; + } + + private function escapeClassName(string $class): string + { + if (str_contains($class, '@')) { + // anonymous class: replace all PSR6-reserved characters + return str_replace(["\0", '\\', '/', '@', ':', '{', '}', '(', ')'], '.', $class); + } + + return str_replace('\\', '.', $class); + } +} diff --git a/src/Document/Metadata/MetadataFactory.php b/src/Document/Metadata/MetadataFactory.php index 50f083e..3859b0f 100644 --- a/src/Document/Metadata/MetadataFactory.php +++ b/src/Document/Metadata/MetadataFactory.php @@ -4,57 +4,12 @@ namespace Setono\SyliusMeilisearchPlugin\Document\Metadata; -use Psr\Cache\CacheItemPoolInterface; use Setono\SyliusMeilisearchPlugin\Document\Document; -use Webmozart\Assert\Assert; final class MetadataFactory implements MetadataFactoryInterface { - /** - * The loaded metadata, indexed by class name - * - * @var array, MetadataInterface> - */ - private array $loadedClasses = []; - - public function __construct(private readonly CacheItemPoolInterface $cache) - { - } - public function getMetadataFor(string|Document $document): MetadataInterface { - if ($document instanceof Document) { - $document = $document::class; - } - - if (isset($this->loadedClasses[$document])) { - return $this->loadedClasses[$document]; - } - - $cacheItem = $this->cache->getItem($this->escapeClassName($document)); - if ($cacheItem->isHit()) { - $metadata = $cacheItem->get(); - Assert::isInstanceOf($metadata, MetadataInterface::class); - - $this->loadedClasses[$document] = $metadata; - - return $this->loadedClasses[$document]; - } - - $metadata = new Metadata($document); - - $this->cache->save($cacheItem->set($metadata)); - - return $this->loadedClasses[$document] = $metadata; - } - - private function escapeClassName(string $class): string - { - if (str_contains($class, '@')) { - // anonymous class: replace all PSR6-reserved characters - return str_replace(["\0", '\\', '/', '@', ':', '{', '}', '(', ')'], '.', $class); - } - - return str_replace('\\', '.', $class); + return new Metadata($document); } } diff --git a/src/Resources/config/services/document.xml b/src/Resources/config/services/document.xml index 52c5e03..49e422e 100644 --- a/src/Resources/config/services/document.xml +++ b/src/Resources/config/services/document.xml @@ -2,11 +2,11 @@ - + - - - + diff --git a/tests/Application/config/packages/setono_sylius_meilisearch.yaml b/tests/Application/config/packages/setono_sylius_meilisearch.yaml index 3dc7b2d..0cae12f 100644 --- a/tests/Application/config/packages/setono_sylius_meilisearch.yaml +++ b/tests/Application/config/packages/setono_sylius_meilisearch.yaml @@ -16,3 +16,8 @@ setono_sylius_meilisearch: autocomplete: indexes: - products + +when@prod: + setono_sylius_meilisearch: + metadata: + cache: true diff --git a/tests/Unit/Document/Metadata/CachedMetadataFactoryTest.php b/tests/Unit/Document/Metadata/CachedMetadataFactoryTest.php new file mode 100644 index 0000000..e5764a8 --- /dev/null +++ b/tests/Unit/Document/Metadata/CachedMetadataFactoryTest.php @@ -0,0 +1,62 @@ +prophesize(CacheItemPoolInterface::class); + $baseFactory = $this->prophesize(MetadataFactoryInterface::class); + $factory = new CachedMetadataFactory($baseFactory->reveal(), $cacheItemPool->reveal()); + + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItemPool + ->getItem('Setono.SyliusMeilisearchPlugin.Tests.Unit.Document.Metadata.Document') + ->willReturn($cacheItem) + ; + $cacheItem->isHit()->willReturn(false); + $metadata = new Metadata(Document::class); + $baseFactory->getMetadataFor(Document::class)->willReturn($metadata); + $cacheItem->set($metadata)->shouldBeCalled()->willReturn($cacheItem); + $cacheItemPool->save($cacheItem->reveal())->shouldBeCalled(); + + self::assertEquals( + new Metadata(Document::class), + $factory->getMetadataFor(Document::class), + ); + } + + public function testItFetchesMetadataFromCache(): void + { + $cacheItemPool = $this->prophesize(CacheItemPoolInterface::class); + $baseFactory = $this->prophesize(MetadataFactoryInterface::class); + $factory = new CachedMetadataFactory($baseFactory->reveal(), $cacheItemPool->reveal()); + + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItemPool + ->getItem('Setono.SyliusMeilisearchPlugin.Tests.Unit.Document.Metadata.Document') + ->willReturn($cacheItem) + ; + $cacheItem->isHit()->willReturn(true); + $metadata = new Metadata(Document::class); + $cacheItem->get()->willReturn($metadata); + + self::assertEquals( + $metadata, + $factory->getMetadataFor(Document::class), + ); + } +} diff --git a/tests/Unit/Document/Metadata/Document.php b/tests/Unit/Document/Metadata/Document.php new file mode 100644 index 0000000..898efc1 --- /dev/null +++ b/tests/Unit/Document/Metadata/Document.php @@ -0,0 +1,27 @@ +getMetadataFor(Document::class)); + } +} diff --git a/tests/Unit/Document/Metadata/MetadataTest.php b/tests/Unit/Document/Metadata/MetadataTest.php index 30f6839..ec3ffe8 100644 --- a/tests/Unit/Document/Metadata/MetadataTest.php +++ b/tests/Unit/Document/Metadata/MetadataTest.php @@ -5,19 +5,11 @@ namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\Document\Metadata; use PHPUnit\Framework\TestCase; -use Setono\SyliusMeilisearchPlugin\Document\Attribute\Facet; -use Setono\SyliusMeilisearchPlugin\Document\Attribute\Filterable; -use Setono\SyliusMeilisearchPlugin\Document\Attribute\Searchable; -use Setono\SyliusMeilisearchPlugin\Document\Attribute\Sortable; -use Setono\SyliusMeilisearchPlugin\Document\Document as BaseDocument; use Setono\SyliusMeilisearchPlugin\Document\Metadata\Metadata; final class MetadataTest extends TestCase { - /** - * @test - */ - public function it_resolves_attributes(): void + public function testItResolvesAttributes(): void { $metadata = new Metadata(Document::class); @@ -37,19 +29,3 @@ public function it_resolves_attributes(): void self::assertArrayHasKey('price', $metadata->getSortableAttributes()); } } - -final class Document extends BaseDocument -{ - #[Searchable] - public ?string $name = null; - - #[Facet] - public ?string $size = null; - - #[Facet] - #[Sortable] - public ?int $price = null; - - #[Filterable] - public array $taxons = []; -}