diff --git a/src/Cache/ChainCache.php b/src/Cache/ChainCache.php index d97fdb4a..57e60984 100644 --- a/src/Cache/ChainCache.php +++ b/src/Cache/ChainCache.php @@ -11,9 +11,9 @@ * @internal * * @template EntryType - * @implements CacheInterface + * @implements WarmupCache */ -final class ChainCache implements CacheInterface +final class ChainCache implements WarmupCache { /** @var array> */ private array $delegates; @@ -29,6 +29,15 @@ public function __construct(CacheInterface ...$delegates) $this->count = count($delegates); } + public function warmup(): void + { + foreach ($this->delegates as $delegate) { + if ($delegate instanceof WarmupCache) { + $delegate->warmup(); + } + } + } + public function get($key, $default = null): mixed { foreach ($this->delegates as $i => $delegate) { diff --git a/src/Cache/Compiled/CompiledPhpFileCache.php b/src/Cache/Compiled/CompiledPhpFileCache.php index 88cbe179..39ce5d57 100644 --- a/src/Cache/Compiled/CompiledPhpFileCache.php +++ b/src/Cache/Compiled/CompiledPhpFileCache.php @@ -7,11 +7,11 @@ use CuyZ\Valinor\Cache\Exception\CacheDirectoryNotWritable; use CuyZ\Valinor\Cache\Exception\CompiledPhpCacheFileNotWritten; use CuyZ\Valinor\Cache\Exception\CorruptedCompiledPhpCacheFile; +use CuyZ\Valinor\Cache\WarmupCache; use DateInterval; use DateTime; use Error; use FilesystemIterator; -use Psr\SimpleCache\CacheInterface; use Traversable; use function bin2hex; @@ -30,9 +30,9 @@ * @internal * * @template EntryType - * @implements CacheInterface + * @implements WarmupCache */ -final class CompiledPhpFileCache implements CacheInterface +final class CompiledPhpFileCache implements WarmupCache { private const TEMPORARY_DIR_PERMISSION = 510; @@ -46,6 +46,11 @@ public function __construct( private CacheCompiler $compiler ) {} + public function warmup(): void + { + $this->createTemporaryDir(); + } + public function has($key): bool { $filename = $this->path($key); @@ -74,11 +79,7 @@ public function set($key, $value, $ttl = null): bool $code = $this->compile($value, $ttl); - $tmpDir = $this->cacheDir . DIRECTORY_SEPARATOR . '.valinor.tmp'; - - if (! is_dir($tmpDir) && ! @mkdir($tmpDir, self::TEMPORARY_DIR_PERMISSION, true)) { - throw new CacheDirectoryNotWritable($this->cacheDir); - } + $tmpDir = $this->createTemporaryDir(); /** @infection-ignore-all */ $tmpFilename = $tmpDir . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16)); @@ -228,6 +229,17 @@ private function getFile(string $filename): PhpCacheFile return $this->files[$filename]; } + private function createTemporaryDir(): string + { + $tmpDir = $this->cacheDir . DIRECTORY_SEPARATOR . '.valinor.tmp'; + + if (! is_dir($tmpDir) && ! @mkdir($tmpDir, self::TEMPORARY_DIR_PERMISSION, true)) { + throw new CacheDirectoryNotWritable($this->cacheDir); + } + + return $tmpDir; + } + private function path(string $key): string { /** @infection-ignore-all */ diff --git a/src/Cache/FileSystemCache.php b/src/Cache/FileSystemCache.php index 0082308e..ded11aa1 100644 --- a/src/Cache/FileSystemCache.php +++ b/src/Cache/FileSystemCache.php @@ -20,9 +20,9 @@ * @api * * @template EntryType - * @implements CacheInterface + * @implements WarmupCache */ -final class FileSystemCache implements CacheInterface +final class FileSystemCache implements WarmupCache { /** @var array> */ private array $delegates; @@ -39,6 +39,15 @@ public function __construct(string $cacheDir = null) ]; } + public function warmup(): void + { + foreach ($this->delegates as $delegate) { + if ($delegate instanceof WarmupCache) { + $delegate->warmup(); + } + } + } + public function has($key): bool { foreach ($this->delegates as $delegate) { diff --git a/src/Cache/FileWatchingCache.php b/src/Cache/FileWatchingCache.php index 770d38c6..417762bb 100644 --- a/src/Cache/FileWatchingCache.php +++ b/src/Cache/FileWatchingCache.php @@ -29,9 +29,9 @@ * * @phpstan-type TimestampsArray = array * @template EntryType - * @implements CacheInterface + * @implements WarmupCache */ -final class FileWatchingCache implements CacheInterface +final class FileWatchingCache implements WarmupCache { /** @var array */ private array $timestamps = []; @@ -41,6 +41,13 @@ public function __construct( private CacheInterface $delegate ) {} + public function warmup(): void + { + if ($this->delegate instanceof WarmupCache) { + $this->delegate->warmup(); + } + } + public function has($key): bool { foreach ($this->timestamps($key) as $fileName => $timestamp) { diff --git a/src/Cache/KeySanitizerCache.php b/src/Cache/KeySanitizerCache.php index bd83af13..6950e446 100644 --- a/src/Cache/KeySanitizerCache.php +++ b/src/Cache/KeySanitizerCache.php @@ -15,9 +15,9 @@ * @internal * * @template EntryType - * @implements CacheInterface + * @implements WarmupCache */ -final class KeySanitizerCache implements CacheInterface +final class KeySanitizerCache implements WarmupCache { private static string $version; @@ -38,6 +38,13 @@ public function __construct( $this->sanitize = static fn (string $key) => sha1("$key." . self::$version ??= PHP_VERSION . '/' . Package::version()); } + public function warmup(): void + { + if ($this->delegate instanceof WarmupCache) { + $this->delegate->warmup(); + } + } + public function get($key, $default = null): mixed { return $this->delegate->get(($this->sanitize)($key), $default); diff --git a/src/Cache/Warmup/RecursiveCacheWarmupService.php b/src/Cache/Warmup/RecursiveCacheWarmupService.php index f22beca3..4648fc68 100644 --- a/src/Cache/Warmup/RecursiveCacheWarmupService.php +++ b/src/Cache/Warmup/RecursiveCacheWarmupService.php @@ -5,15 +5,17 @@ namespace CuyZ\Valinor\Cache\Warmup; use CuyZ\Valinor\Cache\Exception\InvalidSignatureToWarmup; +use CuyZ\Valinor\Cache\WarmupCache; use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory; use CuyZ\Valinor\Mapper\Tree\Builder\ObjectImplementations; +use CuyZ\Valinor\Type\ClassType; use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Parser\Exception\InvalidType; use CuyZ\Valinor\Type\Parser\TypeParser; use CuyZ\Valinor\Type\Type; -use CuyZ\Valinor\Type\ClassType; use CuyZ\Valinor\Type\Types\InterfaceType; +use Psr\SimpleCache\CacheInterface; use function in_array; @@ -23,8 +25,12 @@ final class RecursiveCacheWarmupService /** @var list */ private array $classesWarmedUp = []; + private bool $warmupWasDone = false; + public function __construct( private TypeParser $parser, + /** @var CacheInterface */ + private CacheInterface $cache, private ObjectImplementations $implementations, private ClassDefinitionRepository $classDefinitionRepository, private ObjectBuilderFactory $objectBuilderFactory @@ -32,6 +38,14 @@ public function __construct( public function warmup(string ...$signatures): void { + if (! $this->warmupWasDone) { + $this->warmupWasDone = true; + + if ($this->cache instanceof WarmupCache) { + $this->cache->warmup(); + } + } + foreach ($signatures as $signature) { try { $this->warmupType($this->parser->parse($signature)); diff --git a/src/Cache/WarmupCache.php b/src/Cache/WarmupCache.php new file mode 100644 index 00000000..97a46843 --- /dev/null +++ b/src/Cache/WarmupCache.php @@ -0,0 +1,16 @@ + + */ +interface WarmupCache extends CacheInterface +{ + public function warmup(): void; +} diff --git a/src/Library/Container.php b/src/Library/Container.php index 3c674421..eb19d8bb 100644 --- a/src/Library/Container.php +++ b/src/Library/Container.php @@ -200,6 +200,7 @@ public function __construct(Settings $settings) RecursiveCacheWarmupService::class => fn () => new RecursiveCacheWarmupService( $this->get(TypeParser::class), + $this->get(CacheInterface::class), $this->get(ObjectImplementations::class), $this->get(ClassDefinitionRepository::class), $this->get(ObjectBuilderFactory::class) diff --git a/tests/Fake/Cache/FakeCacheWithWarmup.php b/tests/Fake/Cache/FakeCacheWithWarmup.php new file mode 100644 index 00000000..cca5a2ab --- /dev/null +++ b/tests/Fake/Cache/FakeCacheWithWarmup.php @@ -0,0 +1,65 @@ + + */ +final class FakeCacheWithWarmup implements WarmupCache +{ + private int $warmupCount = 0; + + public function timesWarmupWasCalled(): int + { + return $this->warmupCount; + } + + public function warmup(): void + { + $this->warmupCount++; + } + + public function get($key, $default = null): mixed + { + return null; + } + + public function set($key, $value, $ttl = null): bool + { + return false; + } + + public function delete($key): bool + { + return false; + } + + public function clear(): bool + { + return false; + } + + public function getMultiple($keys, $default = null): iterable + { + return []; + } + + public function setMultiple($values, $ttl = null): bool + { + return false; + } + + public function deleteMultiple($keys): bool + { + return false; + } + + public function has($key): bool + { + return false; + } +} diff --git a/tests/Integration/Cache/CacheWarmupTest.php b/tests/Integration/Cache/CacheWarmupTest.php index 10721382..05307030 100644 --- a/tests/Integration/Cache/CacheWarmupTest.php +++ b/tests/Integration/Cache/CacheWarmupTest.php @@ -7,6 +7,7 @@ use CuyZ\Valinor\Cache\Exception\InvalidSignatureToWarmup; use CuyZ\Valinor\MapperBuilder; use CuyZ\Valinor\Tests\Fake\Cache\FakeCache; +use CuyZ\Valinor\Tests\Fake\Cache\FakeCacheWithWarmup; use CuyZ\Valinor\Tests\Integration\IntegrationTest; use DateTimeInterface; @@ -24,6 +25,26 @@ protected function setUp(): void $this->mapper = (new MapperBuilder())->withCache($this->cache); } + public function test_cache_warmup_is_called_only_once(): void + { + $cache = new FakeCacheWithWarmup(); + $mapper = (new MapperBuilder())->withCache($cache); + + $mapper->warmup(); + $mapper->warmup(); + + self::assertSame(1, $cache->timesWarmupWasCalled()); + } + + /** + * @doesNotPerformAssertions + */ + public function test_cache_warmup_does_not_call_delegate_warmup_if_not_handled(): void + { + $mapper = new MapperBuilder(); // no cache registered + $mapper->warmup(); + } + public function test_will_warmup_type_parser_cache_for_object_with_properties(): void { $this->mapper->warmup(ObjectToWarmupWithProperties::class); diff --git a/tests/Unit/Cache/Compiled/CompiledPhpFileCacheTest.php b/tests/Unit/Cache/Compiled/CompiledPhpFileCacheTest.php index a83d6c13..b67ab37a 100644 --- a/tests/Unit/Cache/Compiled/CompiledPhpFileCacheTest.php +++ b/tests/Unit/Cache/Compiled/CompiledPhpFileCacheTest.php @@ -35,6 +35,15 @@ protected function setUp(): void $this->cache = new CompiledPhpFileCache(vfsStream::url('cache-dir'), new FakeCacheCompiler()); } + public function test_warmup_creates_temporary_dir(): void + { + self::assertFalse($this->files->hasChild('.valinor.tmp')); + + $this->cache->warmup(); + + self::assertTrue($this->files->hasChild('.valinor.tmp')); + } + public function test_set_cache_sets_cache(): void { self::assertFalse($this->cache->has('foo')); diff --git a/tests/Unit/Cache/Compiled/FileWatchingCacheTest.php b/tests/Unit/Cache/Compiled/FileWatchingCacheTest.php index 0a904eea..55084adc 100644 --- a/tests/Unit/Cache/Compiled/FileWatchingCacheTest.php +++ b/tests/Unit/Cache/Compiled/FileWatchingCacheTest.php @@ -6,6 +6,7 @@ use CuyZ\Valinor\Cache\FileWatchingCache; use CuyZ\Valinor\Tests\Fake\Cache\FakeCache; +use CuyZ\Valinor\Tests\Fake\Cache\FakeCacheWithWarmup; use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition; use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition; use org\bovigo\vfs\vfsStream; @@ -36,6 +37,27 @@ protected function setUp(): void $this->cache = new FileWatchingCache($this->delegateCache); } + public function test_cache_warmup_calls_delegate_warmup(): void + { + $delegate = new FakeCacheWithWarmup(); + $cache = new FileWatchingCache($delegate); + + $cache->warmup(); + + self::assertSame(1, $delegate->timesWarmupWasCalled()); + } + + /** + * @doesNotPerformAssertions + */ + public function test_cache_warmup_does_not_call_delegate_warmup_if_not_handled(): void + { + $delegate = new FakeCache(); + $cache = new FileWatchingCache($delegate); + + $cache->warmup(); + } + public function test_value_can_be_fetched_and_deleted(): void { $key = 'foo'; diff --git a/tests/Unit/Cache/FileSystemCacheTest.php b/tests/Unit/Cache/FileSystemCacheTest.php index 1aa16695..1363f152 100644 --- a/tests/Unit/Cache/FileSystemCacheTest.php +++ b/tests/Unit/Cache/FileSystemCacheTest.php @@ -8,12 +8,14 @@ use CuyZ\Valinor\Definition\ClassDefinition; use CuyZ\Valinor\Definition\FunctionDefinition; use CuyZ\Valinor\Tests\Fake\Cache\FakeCache; +use CuyZ\Valinor\Tests\Fake\Cache\FakeCacheWithWarmup; use CuyZ\Valinor\Tests\Fake\Cache\FakeFailingCache; use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition; use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamDirectory; use PHPUnit\Framework\TestCase; +use Psr\SimpleCache\CacheInterface; use function iterator_to_array; @@ -31,7 +33,26 @@ protected function setUp(): void $this->files = vfsStream::setup('cache-dir'); $this->cache = new FileSystemCache($this->files->url()); - $this->injectFakeCache(); + $this->injectFakeCache([ + '*' => new FakeCache(), + ClassDefinition::class => new FakeCache(), + FunctionDefinition::class => new FakeCache(), + ]); + } + + public function test_cache_warmup_calls_delegates_warmup(): void + { + $cacheWithWarmup = new FakeCacheWithWarmup(); + + $this->injectFakeCache([ + '*' => new FakeCache(), + ClassDefinition::class => $cacheWithWarmup, + FunctionDefinition::class => new FakeCache(), + ]); + + $this->cache->warmup(); + + self::assertSame(1, $cacheWithWarmup->timesWarmupWasCalled()); } public function test_cache_entries_are_handled_properly(): void @@ -108,7 +129,11 @@ public function test_multiple_cache_entries_are_handled_properly(): void public function test_methods_returns_false_if_delegates_fail(): void { - $this->injectFakeCache(true); + $this->injectFakeCache([ + '*' => new FakeCache(), + ClassDefinition::class => new FakeCache(), + FunctionDefinition::class => new FakeFailingCache(), + ]); $classDefinition = FakeClassDefinition::new(); $functionDefinition = FakeFunctionDefinition::new(); @@ -136,14 +161,13 @@ public function test_get_non_existing_entry_returns_default_value(): void self::assertSame($defaultValue, $this->cache->get('non-existing-entry', $defaultValue)); } - private function injectFakeCache(bool $withFailingCache = false): void + /** + * @param array> $caches + */ + private function injectFakeCache(array $caches): void { - (function () use ($withFailingCache) { - $this->delegates = [ - '*' => new FakeCache(), - ClassDefinition::class => new FakeCache(), - FunctionDefinition::class => $withFailingCache ? new FakeFailingCache() : new FakeCache(), - ]; + (function () use ($caches) { + $this->delegates = $caches; })->call($this->cache); } }