From ccf09fd33433e065ca7ad26cc90e433dc8d1ae84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 23 May 2022 22:01:40 +0200 Subject: [PATCH] feat: introduce method to warm the cache up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This new method can be used for instance in a pipeline during the build and deployment of the application. The cache has to be registered first, otherwise the warmup will end up being useless. ```php $cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir'); $mapperBuilder = (new \CuyZ\Valinor\MapperBuilder())->withCache($cache); // During the build: $mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class); // In the application: $mapper->mapper()->map(SomeClass::class, [/* … */]); ``` Co-authored-by: Romain Canon --- README.md | 20 +++ .../Exception/InvalidSignatureToWarmup.php | 21 ++++ .../Warmup/RecursiveCacheWarmupService.php | 79 ++++++++++++ src/Library/Container.php | 11 ++ src/MapperBuilder.php | 18 ++- tests/Fake/Cache/FakeCache.php | 15 +++ tests/Fixture/Object/StringableObject.php | 4 +- tests/Integration/Cache/CacheWarmupTest.php | 117 ++++++++++++++++++ 8 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 src/Cache/Exception/InvalidSignatureToWarmup.php create mode 100644 src/Cache/Warmup/RecursiveCacheWarmupService.php create mode 100644 tests/Integration/Cache/CacheWarmupTest.php diff --git a/README.md b/README.md index 45c2b45c..ec8b70ee 100644 --- a/README.md +++ b/README.md @@ -920,6 +920,26 @@ if ($isApplicationInDevelopmentEnvironment) { ->map(SomeClass::class, [/* … */]); ``` +### Warming up cache + +The cache can be warmed up, for instance in a pipeline during the build and +deployment of the application. + +> **Note** The cache has to be registered first, otherwise the warmup will end +> up being useless. + +```php +$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir'); + +$mapperBuilder = (new \CuyZ\Valinor\MapperBuilder())->withCache($cache); + +// During the build: +$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class); + +// In the application: +$mapper->mapper()->map(SomeClass::class, [/* … */]); +``` + ## Static analysis To help static analysis of a codebase using this library, an extension for diff --git a/src/Cache/Exception/InvalidSignatureToWarmup.php b/src/Cache/Exception/InvalidSignatureToWarmup.php new file mode 100644 index 00000000..7497ce71 --- /dev/null +++ b/src/Cache/Exception/InvalidSignatureToWarmup.php @@ -0,0 +1,21 @@ +getMessage()}", + 1653330261, + $exception + ); + } +} diff --git a/src/Cache/Warmup/RecursiveCacheWarmupService.php b/src/Cache/Warmup/RecursiveCacheWarmupService.php new file mode 100644 index 00000000..c7ee05d3 --- /dev/null +++ b/src/Cache/Warmup/RecursiveCacheWarmupService.php @@ -0,0 +1,79 @@ + */ + private array $classesWarmedUp = []; + + public function __construct(TypeParser $parser, ClassDefinitionRepository $classDefinitionRepository) + { + $this->parser = $parser; + $this->classDefinitionRepository = $classDefinitionRepository; + } + + public function warmup(string ...$signatures): void + { + foreach ($signatures as $signature) { + try { + $this->warmupType($this->parser->parse($signature)); + } catch (InvalidType $exception) { + throw new InvalidSignatureToWarmup($signature, $exception); + } + } + } + + private function warmupType(Type $type): void + { + if ($type instanceof ClassType) { + $this->warmupClassType($type); + } + + if ($type instanceof CompositeType) { + foreach ($type->traverse() as $subType) { + $this->warmupType($subType); + } + } + } + + private function warmupClassType(ClassType $type): void + { + if (in_array($type->className(), $this->classesWarmedUp, true)) { + return; + } + + $this->classesWarmedUp[] = $type->className(); + + $classDefinition = $this->classDefinitionRepository->for($type); + + foreach ($classDefinition->properties() as $property) { + $this->warmupType($property->type()); + } + + foreach ($classDefinition->methods() as $method) { + $this->warmupType($method->returnType()); + + foreach ($method->parameters() as $parameter) { + $this->warmupType($parameter->type()); + } + } + } +} diff --git a/src/Library/Container.php b/src/Library/Container.php index cd80e6e3..94f5cbc2 100644 --- a/src/Library/Container.php +++ b/src/Library/Container.php @@ -7,6 +7,7 @@ use CuyZ\Valinor\Cache\ChainCache; use CuyZ\Valinor\Cache\RuntimeCache; use CuyZ\Valinor\Cache\VersionedCache; +use CuyZ\Valinor\Cache\Warmup\RecursiveCacheWarmupService; use CuyZ\Valinor\Definition\FunctionsContainer; use CuyZ\Valinor\Definition\Repository\AttributesRepository; use CuyZ\Valinor\Definition\Repository\Cache\CacheClassDefinitionRepository; @@ -201,6 +202,11 @@ public function __construct(Settings $settings) TemplateParser::class => fn () => new BasicTemplateParser(), + RecursiveCacheWarmupService::class => fn () => new RecursiveCacheWarmupService( + $this->get(TypeParser::class), + $this->get(ClassDefinitionRepository::class), + ), + CacheInterface::class => function () use ($settings) { $cache = new RuntimeCache(); @@ -218,6 +224,11 @@ public function treeMapper(): TreeMapper return $this->get(TreeMapper::class); } + public function cacheWarmupService(): RecursiveCacheWarmupService + { + return $this->get(RecursiveCacheWarmupService::class); + } + /** * @template T of object * @param class-string $name diff --git a/src/MapperBuilder.php b/src/MapperBuilder.php index 1c8d6700..85dcddba 100644 --- a/src/MapperBuilder.php +++ b/src/MapperBuilder.php @@ -250,6 +250,17 @@ public function withCacheDir(string $cacheDir): self return $this->withCache(new FileSystemCache($cacheDir)); } + /** + * Warms up the injected cache implementation with the provided class names. + * + * By passing a class which contains recursive objects, every nested object + * will be cached as well. + */ + public function warmup(string ...$signatures): void + { + $this->container()->cacheWarmupService()->warmup(...$signatures); + } + /** * @deprecated It is not advised to use DoctrineAnnotation when using * PHP >= 8, you should use built-in PHP attributes instead. @@ -272,7 +283,7 @@ public function bind(callable $callback): self public function mapper(): TreeMapper { - return ($this->container ??= new Container($this->settings))->treeMapper(); + return $this->container()->treeMapper(); } public function __clone() @@ -280,4 +291,9 @@ public function __clone() $this->settings = clone $this->settings; unset($this->container); } + + private function container(): Container + { + return ($this->container ??= new Container($this->settings)); + } } diff --git a/tests/Fake/Cache/FakeCache.php b/tests/Fake/Cache/FakeCache.php index 0ff28758..59cbe110 100644 --- a/tests/Fake/Cache/FakeCache.php +++ b/tests/Fake/Cache/FakeCache.php @@ -6,6 +6,8 @@ use Psr\SimpleCache\CacheInterface; +use function count; + /** * @implements CacheInterface */ @@ -14,6 +16,8 @@ final class FakeCache implements CacheInterface /** @var mixed[] */ private array $entries = []; + private int $timesSetWasCalled = 0; + /** @var array */ private array $timesEntryWasSet = []; @@ -30,6 +34,16 @@ public function timesEntryWasFetched(string $key): int return $this->timesEntryWasFetched[$key] ?? 0; } + public function timeSetWasCalled(): int + { + return $this->timesSetWasCalled; + } + + public function countEntries(): int + { + return count($this->entries); + } + public function get($key, $default = null) { $this->timesEntryWasFetched[$key] ??= 0; @@ -42,6 +56,7 @@ public function set($key, $value, $ttl = null): bool { $this->entries[$key] = $value; + $this->timesSetWasCalled++; $this->timesEntryWasSet[$key] ??= 0; $this->timesEntryWasSet[$key]++; diff --git a/tests/Fixture/Object/StringableObject.php b/tests/Fixture/Object/StringableObject.php index 58fc2c31..5fcf41c3 100644 --- a/tests/Fixture/Object/StringableObject.php +++ b/tests/Fixture/Object/StringableObject.php @@ -4,9 +4,7 @@ namespace CuyZ\Valinor\Tests\Fixture\Object; -/** - * @PHP8.0 implement Stringable - */ +// @PHP8.0 implement Stringable final class StringableObject { private string $value; diff --git a/tests/Integration/Cache/CacheWarmupTest.php b/tests/Integration/Cache/CacheWarmupTest.php new file mode 100644 index 00000000..16454a0b --- /dev/null +++ b/tests/Integration/Cache/CacheWarmupTest.php @@ -0,0 +1,117 @@ +cache = new FakeCache(); + $this->mapper = (new MapperBuilder())->withCache($this->cache); + } + + public function test_will_warmup_type_parser_cache(): void + { + $this->mapper->warmup(ObjectToWarmup::class); + $this->mapper->warmup(ObjectToWarmup::class, SomeObjectJ::class); + + self::assertSame(11, $this->cache->countEntries()); + self::assertSame(11, $this->cache->timeSetWasCalled()); + } + + public function test_warmup_invalid_signature_throws_exception(): void + { + $this->expectException(InvalidSignatureToWarmup::class); + $this->expectExceptionCode(1653330261); + $this->expectExceptionMessage('Cannot warm up invalid signature `SomeInvalidClass`: Cannot parse unknown symbol `SomeInvalidClass`.'); + + $this->mapper->warmup('SomeInvalidClass'); + } +} + +final class ObjectToWarmup +{ + public string $string; + + public SomeObjectA $objectA; + + /** @var array */ + public array $arrayOfStrings; + + /** @var array */ + public array $arrayOfObjects; + + /** @var list */ + public array $listOfObjects; + + /** @var iterable */ + public iterable $iterableOfObjects; + + /** @var array{foo: string, object: SomeObjectE} */ + public array $shapedArrayContainingObject; + + /** @var string|SomeObjectF */ + public $unionContainingObject; // @PHP8.0 Native union + + /** @var SomeObjectG&DateTimeInterface */ + public object $intersectionOfObjects; + + public static function someMethod(string $string, SomeObjectH $object): SomeObjectI + { + return new SomeObjectI(); + } +} + +class SomeObjectA +{ +} + +class SomeObjectB +{ +} + +class SomeObjectC +{ +} + +class SomeObjectD +{ +} + +class SomeObjectE +{ +} + +class SomeObjectF +{ +} + +class SomeObjectG +{ +} + +class SomeObjectH +{ +} + +class SomeObjectI +{ +} + +class SomeObjectJ +{ +}