From 266f398a5e6cc9b04a39d72ed9250f9a06a49cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kon=C3=A1=C5=A1?= Date: Sun, 7 Jul 2024 19:25:39 +0200 Subject: [PATCH] Replace doctrine/cache with PSR-6 https://github.com/doctrine/orm/blob/3.0.x/UPGRADE.md#bc-break-remove-support-for-doctrine-cache --- .docs/README.md | 101 +++++++------- composer.json | 11 +- src/DI/OrmCacheExtension.php | 126 +++++++++++++++--- tests/Cases/DI/OrmCacheExtension.phpt | 112 ++++++++++++---- .../Dummy/DummyCacheConfigurationFactory.php | 8 +- tests/Toolkit/Container.php | 7 +- 6 files changed, 265 insertions(+), 100 deletions(-) diff --git a/.docs/README.md b/.docs/README.md index 9c744fb..0c95956 100755 --- a/.docs/README.md +++ b/.docs/README.md @@ -7,6 +7,7 @@ - [Setup](#setup) - [Relying](#relying) - [Configuration](#configuration) + - [Caching](#caching) - [Mapping](#mapping) - [Attributes](#attributes) - [XML](#xml) @@ -33,10 +34,9 @@ extensions: ## Relying -Take advantage of enpowering this package with 3 extra packages: +Take advantage of empowering this package with 2 extra packages: - `doctrine/dbal` -- `doctrine/cache` - `symfony/console` @@ -58,56 +58,6 @@ extensions: > Doctrine DBAL provides powerful database abstraction layer with many features for database schema introspection, schema management and PDO abstraction. -### `doctrine/cache` - -This package relies on `doctrine/cache`, use prepared [nettrine/cache](https://github.com/contributte/doctrine-cache) integration. - -```bash -composer require nettrine/cache -``` - -```neon -extensions: - nettrine.cache: Nettrine\Cache\DI\CacheExtension -``` - -[Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) needs [Doctrine Cache](https://www.doctrine-project.org/projects/cache.html) to be configured. If you register `nettrine/cache` extension it will detect it automatically. - -`CacheExtension` sets up cache for all important parts: `queryCache`, `resultCache`, `hydrationCache`, `metadataCache` and `secondLevelCache`. - -This is the default configuration, it uses the autowired driver. - -```neon -extensions: - nettrine.orm: Nettrine\ORM\DI\OrmExtension - nettrine.orm.cache: Nettrine\ORM\DI\OrmCacheExtension -``` - -You can also specify a single driver or change the `nettrine.orm.cache.defaultDriver` for all of them. - -```neon -nettrine.orm.cache: - defaultDriver: App\DefaultOrmCacheDriver - queryCache: App\SpecialDriver - resultCache: App\SpecialOtherDriver - hydrationCache: App\SpecialDriver('foo') - metadataCache: @cacheDriver -``` - -`secondLevelCache` uses autowired driver (or `defaultDriver`, if specified) for `CacheConfiguration` setup, but you can also replace it with custom `CacheConfiguration`. - -```neon -nettrine.orm.cache: - secondLevelCache: @cacheConfigurationFactory::create('bar') -``` - -You can turn off `secondLevelCache` by setting it to `false`: - -```neon -nettrine.orm.cache: - secondLevelCache: false -``` - ### `symfony/console` This package relies on `symfony/console`, use prepared [contributte/console](https://github.com/contributte/console) integration. @@ -184,6 +134,53 @@ Take a look at real **Nettrine ORM** configuration example at [contributte/webap 4. You have to configure entity mapping (see below), otherwise you will get `It's a requirement to specify a Metadata Driver` error. +### Caching + +You can set up [caching](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/caching.html) by registering `Nettrine\ORM\DI\OrmCacheExtension`: + +```neon +extensions: + nettrine.orm: Nettrine\ORM\DI\OrmExtension + nettrine.orm.cache: Nettrine\ORM\DI\OrmCacheExtension +``` + +By default, all caches are configured to use the autowired [cache storage](https://doc.nette.org/cs/caching#toc-sluzby-di). You can configure them to other [storage](https://doc.nette.org/cs/caching#toc-uloziste), [cache](https://api.nette.org/caching/master/Nette/Caching/Cache.html) or [cache pool](https://www.php-fig.org/psr/psr-6/#cacheitempoolinterface). + +You can use the `nettrine.orm.cache.defaultDriver` to set the caching driver for all caches that are not explicitly configured or configure the caches one by one. + +If you want to turn cache off, you can use `DevNullStorage` to do so. + +All options are demonstrated in following configuration example: + +```neon +nettrine.orm.cache: + # use different storage + defaultDriver: Nette\Caching\Storages\MemoryStorage + # use cache object + queryCache: Nette\Caching\Cache(namespace: 'orm-query-cache') + # use cache pool object + resultCache: Contributte\Psr6\CachePool(Nette\Caching\Cache(namespace: 'orm-result-cache')) + # use registered service (must be of type `Nette\Caching\Storage`, `Nette\Caching\Cache` or `Psr\Cache\CacheItemPoolInterface`) + hydrationCache: @service + # turn off caching + metadataCache: Nette\Caching\Storages\DevNullStorage +``` + +`secondLevelCache` uses autowired driver (or `defaultDriver`, if specified) for `CacheConfiguration` setup, but you can also replace it with custom `CacheConfiguration`. + +```neon +nettrine.orm.cache: + secondLevelCache: @cacheConfigurationFactory::create('bar') +``` + +You can turn off `secondLevelCache` by setting it to `false`: + +```neon +nettrine.orm.cache: + secondLevelCache: false +``` + + ## Mapping Doctrine ORM needs to know where your entities are located and how they are described (mapping). diff --git a/composer.json b/composer.json index 03a4cc3..392c273 100644 --- a/composer.json +++ b/composer.json @@ -13,22 +13,23 @@ ], "require": { "php": ">=8.1", + "contributte/psr6-caching": "^0.2 || ^0.3", + "doctrine/orm": "^2.14 || ^3.0", "nette/di": "^3.1.2", - "symfony/console": "^5.3.0 || ^6.2.0 || ^7.0.0 ", - "nettrine/cache": "^0.4.0 || ^0.5.0", "nettrine/dbal": "^0.8.0 || ^0.9.0", - "doctrine/orm": "^2.14 || ^3.0", - "doctrine/common":"^3.4.3" + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/console": "^5.3.0 || ^6.2.0 || ^7.0.0 " }, "require-dev": { + "contributte/phpstan": "^0.1", "contributte/qa": "^0.4", "contributte/tester": "^0.3", - "contributte/phpstan": "^0.1", "mockery/mockery": "^1.3.1", "tracy/tracy": "^2.10.3" }, "conflict": { "doctrine/persistence": "<3.0.0", + "nette/caching": "<3.1.0", "nette/di": "<3.0.6", "nette/schema": "<1.1.0" }, diff --git a/src/DI/OrmCacheExtension.php b/src/DI/OrmCacheExtension.php index 9a287c4..73853d1 100644 --- a/src/DI/OrmCacheExtension.php +++ b/src/DI/OrmCacheExtension.php @@ -2,15 +2,19 @@ namespace Nettrine\ORM\DI; -use Doctrine\Common\Cache\Cache; +use Contributte\Psr6\CachePool; +use Contributte\Psr6\CachePoolFactory; use Doctrine\ORM\Cache\CacheConfiguration; use Doctrine\ORM\Cache\DefaultCacheFactory; use Doctrine\ORM\Cache\RegionsConfiguration; +use Nette\Caching\Cache; +use Nette\Caching\Storage; use Nette\DI\Definitions\Definition; use Nette\DI\Definitions\Statement; use Nette\Schema\Expect; use Nette\Schema\Schema; use stdClass; +use Throwable; /** * @property-read stdClass $config @@ -30,7 +34,7 @@ public function getConfigSchema(): Schema ]); } - public function loadConfiguration(): void + public function beforeCompile(): void { // Validates needed extension $this->validate(); @@ -56,7 +60,7 @@ private function loadQueryCacheConfiguration(): void $config = $this->config; $configurationDef = $this->getConfigurationDef(); - $configurationDef->addSetup('setQueryCacheImpl', [ + $configurationDef->addSetup('setQueryCache', [ $this->buildCacheDriver($config->queryCache, 'queryCache'), ]); } @@ -66,7 +70,7 @@ private function loadResultCacheConfiguration(): void $config = $this->config; $configurationDef = $this->getConfigurationDef(); - $configurationDef->addSetup('setResultCacheImpl', [ + $configurationDef->addSetup('setResultCache', [ $this->buildCacheDriver($config->resultCache, 'resultCache'), ]); } @@ -76,7 +80,7 @@ private function loadHydrationCacheConfiguration(): void $config = $this->config; $configurationDef = $this->getConfigurationDef(); - $configurationDef->addSetup('setHydrationCacheImpl', [ + $configurationDef->addSetup('setHydrationCache', [ $this->buildCacheDriver($config->hydrationCache, 'hydrationCache'), ]); } @@ -86,7 +90,7 @@ private function loadMetadataCacheConfiguration(): void $config = $this->config; $configurationDef = $this->getConfigurationDef(); - $configurationDef->addSetup('setMetadataCacheImpl', [ + $configurationDef->addSetup('setMetadataCache', [ $this->buildCacheDriver($config->metadataCache, 'metadataCache'), ]); } @@ -133,15 +137,13 @@ private function loadSecondLevelCacheConfiguration(): void /** * @param string|mixed[]|Statement|null $config */ - private function buildCacheDriver(string|array|Statement|null $config, string $prefix): Definition|string + private function buildCacheDriver(string|array|Statement|null $config, string $prefix): Definition { $builder = $this->getContainerBuilder(); // Driver is defined if ($config !== null && $config !== []) { // Nette converts explicit null to an empty array - return $builder->addDefinition($this->prefix($prefix)) - ->setFactory($config) - ->setAutowired(false); + return $this->buildCacheDriverDefinition($config, $prefix); } // If there is default cache, don't create it @@ -149,15 +151,107 @@ private function buildCacheDriver(string|array|Statement|null $config, string $p return $builder->getDefinition($this->prefix('defaultCache')); } - // Create default driver - if ($this->config->defaultDriver !== null && $this->config->defaultDriver !== []) { // Nette converts explicit null to an empty array - return $builder->addDefinition($this->prefix('defaultCache')) - ->setFactory($this->config->defaultDriver) + return $this->buildCacheDriverDefinition($this->config->defaultDriver, 'defaultCache'); + } + + /** + * @param string|mixed[]|Statement|null $config + */ + private function buildCacheDriverDefinition(string|array|Statement|null $config, string $prefix): Definition + { + $builder = $this->getContainerBuilder(); + + // Driver is defined + if ($config !== null && $config !== []) { // Nette converts explicit null to an empty array + if (is_string($config)) { + $config = $this->resolveCacheDriverDefinitionString($config, $this->prefix($prefix)); + } + + if ($config instanceof Statement) { + $entity = $config->getEntity(); + + if (is_string($entity) && is_a($entity, Storage::class, true)) { + $entity = Cache::class; + $config = new Statement( + $entity, + [ + 'storage' => $config, + 'namespace' => $this->prefix($prefix), + ] + ); + } + + if (is_string($entity) && is_a($entity, Cache::class, true)) { + return $builder->addDefinition($this->prefix($prefix)) + ->setFactory(new Statement(CachePool::class, [$config])) + ->setAutowired(false); + } + } + + return $builder->addDefinition($this->prefix($prefix)) + ->setFactory($config) + ->setAutowired(false); + } + + // No default driver provided, create CacheItemPoolInterface with autowired Storage + + // ICachePoolFactory doesn't have to be registered in DI container + if ($builder->hasDefinition($this->prefix('cachePoolFactory')) === false) { + $builder->addDefinition($this->prefix('cachePoolFactory')) + ->setFactory(CachePoolFactory::class) ->setAutowired(false); } - // No default driver provider, fallback to Cache::class - return '@' . Cache::class; + return $builder->addDefinition($this->prefix($prefix)) + ->setFactory('@' . $this->prefix('cachePoolFactory') . '::create', [$this->prefix($prefix)]) + ->setAutowired(false); + } + + private function resolveCacheDriverDefinitionString(string $config, string $cacheNamespace): string|Statement + { + $builder = $this->getContainerBuilder(); + + if (str_starts_with($config, '@')) { + $service = substr($config, 1); + + if ($builder->hasDefinition($service)) { + $definition = $builder->getDefinition($service); + } else { + try { + $definition = $builder->getDefinitionByType($service); + } catch (Throwable) { + $definition = null; + } + } + + $type = $definition?->getType(); + + if ($type === null) { + return $config; + } + + if (is_a($type, Storage::class, true)) { + return new Statement( + Cache::class, + [ + 'storage' => $config, + 'namespace' => $cacheNamespace, + ] + ); + } + + if (is_a($type, Cache::class, true)) { + return new Statement(CachePool::class, [$config]); + } + + return $config; + } + + if (is_a($config, Storage::class, true)) { + return new Statement($config); + } + + return $config; } } diff --git a/tests/Cases/DI/OrmCacheExtension.phpt b/tests/Cases/DI/OrmCacheExtension.phpt index 457ebf8..28e8093 100644 --- a/tests/Cases/DI/OrmCacheExtension.phpt +++ b/tests/Cases/DI/OrmCacheExtension.phpt @@ -1,16 +1,19 @@ getByType(EntityManagerDecorator::class); - Assert::type(PhpFileCache::class, $em->getConfiguration()->getHydrationCacheImpl()); - Assert::type(PhpFileCache::class, $em->getConfiguration()->getMetadataCacheImpl()); - Assert::type(PhpFileCache::class, $em->getConfiguration()->getQueryCacheImpl()); - Assert::type(PhpFileCache::class, $em->getConfiguration()->getResultCacheImpl()); + Assert::type(CacheItemPoolInterface::class, $em->getConfiguration()->getHydrationCache()); + Assert::type(CacheItemPoolInterface::class, $em->getConfiguration()->getMetadataCache()); + Assert::type(CacheItemPoolInterface::class, $em->getConfiguration()->getQueryCache()); + Assert::type(CacheItemPoolInterface::class, $em->getConfiguration()->getResultCache()); Assert::true($em->getConfiguration()->isSecondLevelCacheEnabled()); Assert::notNull($em->getConfiguration()->getSecondLevelCacheConfiguration()); }); @@ -42,10 +45,10 @@ Toolkit::test(function (): void { $compiler->addExtension('nettrine.orm.cache', new OrmCacheExtension()); $compiler->addConfig([ 'nettrine.orm.cache' => [ - 'defaultDriver' => ArrayCache::class, - 'hydrationCache' => VoidCache::class, - 'metadataCache' => null, - 'queryCache' => ApcuCache::class, + 'defaultDriver' => MemoryStorage::class, + 'hydrationCache' => new Statement(CachePool::class, [new Statement(Cache::class, [1 => 'cache-namespace'])]), // equivalent to config value Contributte\Psr6\CachePool(Nette\Caching\Cache(_, 'cache-namespace')) + 'metadataCache' => new Statement(Cache::class, ['namespace' => 'cache-namespace']), // equivalent to config value Nette\Caching\Cache(namespace: 'cache-namespace') + 'queryCache' => new Statement(MemoryStorage::class), 'secondLevelCache' => [DummyCacheConfigurationFactory::class, 'create'], ], ]); @@ -55,10 +58,10 @@ Toolkit::test(function (): void { /** @var EntityManagerDecorator $em */ $em = $container->getByType(EntityManagerDecorator::class); - Assert::type(VoidCache::class, $em->getConfiguration()->getHydrationCacheImpl()); - Assert::type(ArrayCache::class, $em->getConfiguration()->getMetadataCacheImpl()); - Assert::type(ApcuCache::class, $em->getConfiguration()->getQueryCacheImpl()); - Assert::type(ArrayCache::class, $em->getConfiguration()->getResultCacheImpl()); + Assert::type(CachePool::class, $em->getConfiguration()->getHydrationCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getMetadataCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getQueryCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getResultCache()); Assert::true($em->getConfiguration()->isSecondLevelCacheEnabled()); Assert::notNull($em->getConfiguration()->getSecondLevelCacheConfiguration()); }); @@ -70,9 +73,9 @@ Toolkit::test(function (): void { ->withCompiler(function (Compiler $compiler): void { $compiler->addExtension('nettrine.orm.cache', new OrmCacheExtension()); $compiler->addConfig([ - 'nettrine.orm.cache' => [ - 'secondLevelCache' => false, - ], + 'nettrine.orm.cache' => [ + 'secondLevelCache' => false, + ], ]); }) ->build(); @@ -82,8 +85,71 @@ Toolkit::test(function (): void { Assert::false($em->getConfiguration()->isSecondLevelCacheEnabled()); Assert::null($em->getConfiguration()->getSecondLevelCacheConfiguration()); - Assert::type(PhpFileCache::class, $em->getConfiguration()->getHydrationCacheImpl()); - Assert::type(PhpFileCache::class, $em->getConfiguration()->getMetadataCacheImpl()); - Assert::type(PhpFileCache::class, $em->getConfiguration()->getQueryCacheImpl()); - Assert::type(PhpFileCache::class, $em->getConfiguration()->getResultCacheImpl()); + Assert::type(CachePool::class, $em->getConfiguration()->getHydrationCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getMetadataCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getQueryCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getResultCache()); +}); + +// Provide cache drivers as service links +Toolkit::test(function (): void { + $container = Container::of() + ->withDefaults() + ->withCompiler(function (Compiler $compiler): void { + $compiler->getContainerBuilder()->addDefinition('svcCachePool') + ->setFactory(new Statement(CachePool::class, [new Statement(Cache::class, ['namespace' => 'cache-namespace'])])) + ->setAutowired(false); + $compiler->getContainerBuilder()->addDefinition('svcCache') + ->setFactory(new Statement(Cache::class, ['namespace' => 'cache-namespace'])) + ->setAutowired(false); + $compiler->getContainerBuilder()->addDefinition('svcStorage') + ->setFactory(MemoryStorage::class) + ->setAutowired(false); + $compiler->addExtension('nettrine.orm.cache', new OrmCacheExtension()); + $compiler->addConfig([ + 'nettrine.orm.cache' => [ + 'hydrationCache' => '@svcCachePool', + 'metadataCache' => '@svcCache', + 'queryCache' => '@svcStorage', + ] + ]); + }) + ->build(); + + /** @var EntityManagerDecorator $em */ + $em = $container->getByType(EntityManagerDecorator::class); + + Assert::type(CachePool::class, $em->getConfiguration()->getHydrationCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getMetadataCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getQueryCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getResultCache()); +}); + +// Provide cache drivers as service class links +Toolkit::test(function (): void { + $container = Container::of() + ->withDefaults() + ->withCompiler(function (Compiler $compiler): void { + $compiler->getContainerBuilder()->addDefinition(null) + ->setFactory(new Statement(CachePool::class, [new Statement(Cache::class, ['namespace' => 'cache-namespace'])])); + $compiler->getContainerBuilder()->addDefinition(null) + ->setFactory(new Statement(Cache::class, ['namespace' => 'cache-namespace'])); + $compiler->addExtension('nettrine.orm.cache', new OrmCacheExtension()); + $compiler->addConfig([ + 'nettrine.orm.cache' => [ + 'hydrationCache' => '@' . CachePool::class, + 'metadataCache' => '@' . Cache::class, + 'queryCache' => '@' . Storage::class, + ] + ]); + }) + ->build(); + + /** @var EntityManagerDecorator $em */ + $em = $container->getByType(EntityManagerDecorator::class); + + Assert::type(CachePool::class, $em->getConfiguration()->getHydrationCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getMetadataCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getQueryCache()); + Assert::type(CachePool::class, $em->getConfiguration()->getResultCache()); }); diff --git a/tests/Fixtures/Dummy/DummyCacheConfigurationFactory.php b/tests/Fixtures/Dummy/DummyCacheConfigurationFactory.php index a472a7c..5e959ab 100644 --- a/tests/Fixtures/Dummy/DummyCacheConfigurationFactory.php +++ b/tests/Fixtures/Dummy/DummyCacheConfigurationFactory.php @@ -2,18 +2,20 @@ namespace Tests\Fixtures\Dummy; -use Doctrine\Common\Cache\ArrayCache; +use Contributte\Psr6\CachePool; use Doctrine\ORM\Cache\CacheConfiguration; use Doctrine\ORM\Cache\DefaultCacheFactory; use Doctrine\ORM\Cache\RegionsConfiguration; +use Nette\Caching\Cache; +use Nette\Caching\Storage; final class DummyCacheConfigurationFactory { - public static function create(): CacheConfiguration + public static function create(Storage $cacheStorage): CacheConfiguration { $regionsConfiguration = new RegionsConfiguration(); - $cache = new ArrayCache(); + $cache = new CachePool(new Cache($cacheStorage, self::class)); $cacheFactory = new DefaultCacheFactory($regionsConfiguration, $cache); $cacheConfiguration = new CacheConfiguration(); diff --git a/tests/Toolkit/Container.php b/tests/Toolkit/Container.php index 6f017a5..5c912b8 100644 --- a/tests/Toolkit/Container.php +++ b/tests/Toolkit/Container.php @@ -2,6 +2,7 @@ namespace Tests\Toolkit; +use Nette\Bridges\CacheDI\CacheExtension as NetteCacheExtension; use Nette\DI\Compiler; use Nette\DI\Container as NetteContainer; use Nette\DI\ContainerLoader; @@ -38,9 +39,13 @@ public function withDefaults(): Container public function withDefaultExtensions(): Container { $this->onCompile[] = function (Compiler $compiler): void { + $compiler->addExtension('cache', new NetteCacheExtension(Tests::TEMP_PATH)); $compiler->addExtension('nettrine.dbal', new DbalExtension()); - $compiler->addExtension('nettrine.cache', new CacheExtension()); $compiler->addExtension('nettrine.orm', new OrmExtension()); + + if (class_exists(CacheExtension::class)) { // needed for tests with nettrine/dbal <0.9 + $compiler->addExtension('nettrine.cache', new CacheExtension()); + } }; return $this;