feat: extract file watching feature in own cache implementation

When the application runs in a development environment, the cache
implementation should be decorated with `FileWatchingCache` to prevent
invalid cache entries states, which can result in the library not
behaving as expected (missing property value, callable with outdated
signature, …).

```php
$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir');

if ($isApplicationInDevelopmentEnvironment) {
    $cache = new \CuyZ\Valinor\Cache\FileWatchingCache($cache);
}

(new \CuyZ\Valinor\MapperBuilder())
    ->withCache($cache)
    ->mapper()
    ->map(SomeClass::class, [/* … */]);
```

This behavior now forces to explicitly inject `FileWatchingCache`, when
it was done automatically before; but because it shouldn't be used in
a production environment, it will increase overall performance.
This commit is contained in:
Romain Canon 2022-05-23 00:21:38 +02:00
parent 69ad3f4777
commit 2d70efbfbb
19 changed files with 463 additions and 195 deletions

View File

@ -901,9 +901,19 @@ cache entries into the file system.
> **Note** It is also possible to use any PSR-16 compliant implementation, as
> long as it is capable of caching the entries handled by the library.
When the application runs in a development environment, the cache implementation
should be decorated with `FileWatchingCache`, which will watch the files of the
application and invalidate cache entries when a PHP file is modified by a
developer — preventing the library not behaving as expected when the signature
of a property or a method changes.
```php
$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-directory');
if ($isApplicationInDevelopmentEnvironment) {
$cache = new \CuyZ\Valinor\Cache\FileWatchingCache($cache);
}
(new \CuyZ\Valinor\MapperBuilder())
->withCache($cache)
->mapper()

View File

@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Compiled;
/** @internal */
interface CacheValidationCompiler extends CacheCompiler
{
/**
* @param mixed $value
*/
public function compileValidation($value): string;
}

View File

@ -17,7 +17,6 @@ use Traversable;
use function file_exists;
use function file_put_contents;
use function implode;
use function is_dir;
use function mkdir;
use function rename;
@ -163,26 +162,19 @@ final class CompiledPhpFileCache implements CacheInterface
*/
private function compile($value, $ttl = null): string
{
$validation = [];
$validationCode = 'true';
if ($ttl) {
$time = $ttl instanceof DateInterval
? (new DateTime())->add($ttl)->getTimestamp()
: time() + $ttl;
$validation[] = "time() < $time";
}
if ($this->compiler instanceof CacheValidationCompiler) {
$validation[] = $this->compiler->compileValidation($value);
$validationCode = "time() < $time";
}
$generatedMessage = self::GENERATED_MESSAGE;
$code = $this->compiler->compile($value);
$validationCode = empty($validation)
? 'true'
: '(' . implode(' && ', $validation) . ')';
return <<<PHP
<?php // $generatedMessage

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Compiled;
use function var_export;
/** @internal */
final class MixedValueCacheCompiler implements CacheCompiler
{
public function compile($value): string
{
return var_export($value, true);
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use CuyZ\Valinor\Cache\Compiled\CompiledPhpFileCache;
use CuyZ\Valinor\Cache\Compiled\MixedValueCacheCompiler;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Definition\Repository\Cache\Compiler\ClassDefinitionCompiler;
@ -14,17 +15,19 @@ use Traversable;
use function current;
use function get_class;
use function is_object;
use function next;
use function sys_get_temp_dir;
/**
* @api
*
* @implements CacheInterface<ClassDefinition|FunctionDefinition>
* @template EntryType
* @implements CacheInterface<EntryType>
*/
final class FileSystemCache implements CacheInterface
{
/** @var array<string, CacheInterface<ClassDefinition|FunctionDefinition>> */
/** @var array<string, CacheInterface<EntryType>> */
private array $delegates;
public function __construct(string $cacheDir = null)
@ -33,6 +36,7 @@ final class FileSystemCache implements CacheInterface
// @infection-ignore-all
$this->delegates = [
'*' => new CompiledPhpFileCache($cacheDir . DIRECTORY_SEPARATOR . 'mixed', new MixedValueCacheCompiler()),
ClassDefinition::class => new CompiledPhpFileCache($cacheDir . DIRECTORY_SEPARATOR . 'classes', new ClassDefinitionCompiler()),
FunctionDefinition::class => new CompiledPhpFileCache($cacheDir . DIRECTORY_SEPARATOR . 'functions', new FunctionDefinitionCompiler()),
];
@ -65,7 +69,13 @@ final class FileSystemCache implements CacheInterface
public function set($key, $value, $ttl = null): bool
{
return $this->delegates[get_class($value)]->set($key, $value, $ttl);
$delegate = $this->delegates['*'];
if (is_object($value) && isset($this->delegates[get_class($value)])) {
$delegate = $this->delegates[get_class($value)];
}
return $delegate->set($key, $value, $ttl);
}
public function delete($key): bool
@ -91,7 +101,7 @@ final class FileSystemCache implements CacheInterface
}
/**
* @return Traversable<string, ClassDefinition|FunctionDefinition|null>
* @return Traversable<string, EntryType|null>
*/
public function getMultiple($keys, $default = null): Traversable
{

View File

@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use Psr\SimpleCache\CacheInterface;
use function filemtime;
use function is_string;
/**
* This cache implementation will watch the files of the application and
* invalidate cache entries when a PHP file is modified preventing the library
* not behaving as expected when the signature of a property or a method
* changes.
*
* This is especially useful when the application runs in a development
* environment, where source files are often modified by developers.
*
* It should decorate the original cache implementation and should be given to
* the mapper builder: @see \CuyZ\Valinor\MapperBuilder::withCache
*
* @api
*
* @phpstan-type TimestampsArray = array<string, int>
* @template EntryType
* @implements CacheInterface<EntryType|TimestampsArray>
*/
final class FileWatchingCache implements CacheInterface
{
/** @var CacheInterface<EntryType|TimestampsArray> */
private CacheInterface $delegate;
/** @var array<string, TimestampsArray> */
private array $timestamps = [];
/**
* @param CacheInterface<EntryType|TimestampsArray> $delegate
*/
public function __construct(CacheInterface $delegate)
{
$this->delegate = $delegate;
}
public function has($key): bool
{
foreach ($this->timestamps($key) as $fileName => $timestamp) {
if (@filemtime($fileName) !== $timestamp) {
return false;
}
}
return $this->delegate->has($key);
}
public function get($key, $default = null)
{
return $this->delegate->get($key, $default);
}
public function set($key, $value, $ttl = null): bool
{
$this->saveTimestamps($key, $value);
return $this->delegate->set($key, $value, $ttl);
}
public function delete($key): bool
{
return $this->delegate->delete($key);
}
public function clear(): bool
{
$this->timestamps = [];
return $this->delegate->clear();
}
public function getMultiple($keys, $default = null): iterable
{
return $this->delegate->getMultiple($keys, $default);
}
public function setMultiple($values, $ttl = null): bool
{
foreach ($values as $key => $value) {
$this->saveTimestamps($key, $value);
}
return $this->delegate->setMultiple($values, $ttl);
}
public function deleteMultiple($keys): bool
{
return $this->delegate->deleteMultiple($keys);
}
/**
* @return TimestampsArray
*/
private function timestamps(string $key): array
{
return $this->timestamps[$key] ??= $this->delegate->get("$key.timestamps", []); // @phpstan-ignore-line
}
/**
* @param mixed $value
*/
private function saveTimestamps(string $key, $value): void
{
$this->timestamps[$key] = [];
$fileNames = [];
if ($value instanceof ClassDefinition) {
$reflection = Reflection::class($value->name());
do {
$fileNames[] = $reflection->getFileName();
} while ($reflection = $reflection->getParentClass());
}
if ($value instanceof FunctionDefinition) {
$fileNames[] = $value->fileName();
}
foreach ($fileNames as $fileName) {
if (! is_string($fileName)) {
// @infection-ignore-all
continue;
}
$time = @filemtime($fileName);
// @infection-ignore-all
if (false === $time) {
continue;
}
$this->timestamps[$key][$fileName] = $time;
}
if (! empty($this->timestamps[$key])) {
$this->delegate->set("$key.timestamps", $this->timestamps[$key]);
}
}
}

View File

@ -5,20 +5,17 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Cache\Compiled\CacheCompiler;
use CuyZ\Valinor\Cache\Compiled\CacheValidationCompiler;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\MethodDefinition;
use CuyZ\Valinor\Definition\PropertyDefinition;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use function array_map;
use function assert;
use function filemtime;
use function implode;
use function iterator_to_array;
/** @internal */
final class ClassDefinitionCompiler implements CacheCompiler, CacheValidationCompiler
final class ClassDefinitionCompiler implements CacheCompiler
{
private TypeCompiler $typeCompiler;
@ -67,21 +64,4 @@ final class ClassDefinitionCompiler implements CacheCompiler, CacheValidationCom
)
PHP;
}
public function compileValidation($value): string
{
assert($value instanceof ClassDefinition);
$filename = (Reflection::class($value->name()))->getFileName();
// If the file does not exist it means it's a native class so the
// definition is always valid (for a given PHP version).
if (false === $filename) {
return 'true';
}
$time = filemtime($filename);
return "\\filemtime('$filename') === $time";
}
}

View File

@ -5,14 +5,13 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Cache\Compiled\CacheCompiler;
use CuyZ\Valinor\Cache\Compiled\CacheValidationCompiler;
use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Definition\ParameterDefinition;
use function var_export;
/** @internal */
final class FunctionDefinitionCompiler implements CacheCompiler, CacheValidationCompiler
final class FunctionDefinitionCompiler implements CacheCompiler
{
private TypeCompiler $typeCompiler;
@ -49,21 +48,4 @@ final class FunctionDefinitionCompiler implements CacheCompiler, CacheValidation
)
PHP;
}
public function compileValidation($value): string
{
assert($value instanceof FunctionDefinition);
$fileName = $value->fileName();
// If the file does not exist it means it's a native function so the
// definition is always valid.
if (null === $fileName) {
return 'true';
}
$time = filemtime($fileName);
return "\\filemtime('$fileName') === $time";
}
}

View File

@ -182,9 +182,19 @@ final class MapperBuilder
* It is also possible to use any PSR-16 compliant implementation, as long
* as it is capable of caching the entries handled by the library.
*
* When the application runs in a development environment, the cache
* implementation should be decorated with `FileWatchingCache`, which will
* watch the files of the application and invalidate cache entries when a
* PHP file is modified by a developer preventing the library not behaving
* as expected when the signature of a property or a method changes.
*
* ```php
* $cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir');
*
* if ($isApplicationInDevelopmentEnvironment) {
* $cache = new \CuyZ\Valinor\Cache\FileWatchingCache($cache);
* }
*
* (new \CuyZ\Valinor\MapperBuilder())
* ->withCache($cache)
* ->mapper()

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Fake\Cache\Compiled;
use CuyZ\Valinor\Cache\Compiled\CacheValidationCompiler;
use function is_string;
use function var_export;
final class FakeCacheValidationCompiler implements CacheValidationCompiler
{
public bool $compileValidation = true;
public function compile($value): string
{
assert(is_string($value));
return "'$value'";
}
public function compileValidation($value): string
{
return var_export($this->compileValidation, true);
}
}

View File

@ -6,8 +6,6 @@ namespace CuyZ\Valinor\Tests\Fake\Cache;
use Psr\SimpleCache\CacheInterface;
use function array_keys;
/**
* @implements CacheInterface<mixed>
*/
@ -16,18 +14,27 @@ final class FakeCache implements CacheInterface
/** @var mixed[] */
private array $entries = [];
/**
* @param mixed $value
*/
public function replaceAllBy($value): void
/** @var array<string, int> */
private array $timesEntryWasSet = [];
/** @var array<string, int> */
private array $timesEntryWasFetched = [];
public function timesEntryWasSet(string $key): int
{
foreach (array_keys($this->entries) as $key) {
$this->entries[$key] = $value;
}
return $this->timesEntryWasSet[$key] ?? 0;
}
public function timesEntryWasFetched(string $key): int
{
return $this->timesEntryWasFetched[$key] ?? 0;
}
public function get($key, $default = null)
{
$this->timesEntryWasFetched[$key] ??= 0;
$this->timesEntryWasFetched[$key]++;
return $this->entries[$key] ?? $default;
}
@ -35,6 +42,9 @@ final class FakeCache implements CacheInterface
{
$this->entries[$key] = $value;
$this->timesEntryWasSet[$key] ??= 0;
$this->timesEntryWasSet[$key]++;
return true;
}

View File

@ -10,28 +10,20 @@ use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition;
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithParameterDefaultObjectValue;
use CuyZ\Valinor\Type\Types\NativeStringType;
use Error;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use function get_class;
use function implode;
use function time;
use function unlink;
final class ClassDefinitionCompilerTest extends TestCase
{
private vfsStreamDirectory $files;
private ClassDefinitionCompiler $compiler;
protected function setUp(): void
{
parent::setUp();
$this->files = vfsStream::setup();
$this->compiler = new ClassDefinitionCompiler();
}
@ -107,39 +99,6 @@ final class ClassDefinitionCompilerTest extends TestCase
self::assertSame(ObjectWithParameterDefaultObjectValue::class, $class->name());
}
public function test_modifying_class_definition_file_invalids_compiled_class_definition(): void
{
/** @var class-string $className */
$className = 'SomeClassDefinitionForTest';
$file = (vfsStream::newFile("$className.php"))
->withContent("<?php final class $className {}")
->at($this->files);
include $file->url();
$class = FakeClassDefinition::fromReflection(new ReflectionClass($className));
$validationCode = $this->compiler->compileValidation($class);
$firstValidation = $this->eval($validationCode);
unlink($file->url());
$file->lastModified(time() + 5)->at($this->files);
$secondValidation = $this->eval($validationCode);
self::assertTrue($firstValidation);
self::assertFalse($secondValidation);
}
public function test_compile_validation_for_internal_class_returns_true(): void
{
$code = $this->compiler->compileValidation(FakeClassDefinition::new());
self::assertSame('true', $code);
}
/**
* @return mixed
*/

View File

@ -9,29 +9,19 @@ use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Definition\ParameterDefinition;
use CuyZ\Valinor\Definition\Parameters;
use CuyZ\Valinor\Definition\Repository\Cache\Compiler\FunctionDefinitionCompiler;
use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition;
use CuyZ\Valinor\Type\Types\NativeStringType;
use Error;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use PHPUnit\Framework\TestCase;
use stdClass;
use function time;
use function unlink;
final class FunctionDefinitionCompilerTest extends TestCase
{
private vfsStreamDirectory $files;
private FunctionDefinitionCompiler $compiler;
protected function setUp(): void
{
parent::setUp();
$this->files = vfsStream::setup();
$this->compiler = new FunctionDefinitionCompiler();
}
@ -68,27 +58,6 @@ final class FunctionDefinitionCompilerTest extends TestCase
self::assertInstanceOf(NativeStringType::class, $compiledFunction->returnType());
}
public function test_modifying_function_definition_file_invalids_compiled_function_definition(): void
{
$file = (vfsStream::newFile('foo.php'))
->withContent('<?php function _valinor_test_modifying_function_definition_file_invalids_compiled_function_definition() {}')
->at($this->files);
$class = FakeFunctionDefinition::new($file->url());
$validationCode = $this->compiler->compileValidation($class);
$firstValidation = $this->eval($validationCode);
unlink($file->url());
$file->lastModified(time() + 5)->at($this->files);
$secondValidation = $this->eval($validationCode);
self::assertTrue($firstValidation);
self::assertFalse($secondValidation);
}
/**
* @return FunctionDefinition|bool
*/

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Integration\Cache;
use CuyZ\Valinor\Cache\FileSystemCache;
use CuyZ\Valinor\Cache\FileWatchingCache;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\SimpleObject;
@ -19,6 +20,7 @@ final class CacheInjectionTest extends IntegrationTest
$files = vfsStream::setup('cache-dir');
$cache = new FileSystemCache($files->url());
$cache = new FileWatchingCache($cache);
self::assertFalse($files->hasChildren());

View File

@ -9,7 +9,6 @@ use CuyZ\Valinor\Cache\Exception\CacheDirectoryNotWritable;
use CuyZ\Valinor\Cache\Exception\CompiledPhpCacheFileNotWritten;
use CuyZ\Valinor\Cache\Exception\CorruptedCompiledPhpCacheFile;
use CuyZ\Valinor\Tests\Fake\Cache\Compiled\FakeCacheCompiler;
use CuyZ\Valinor\Tests\Fake\Cache\Compiled\FakeCacheValidationCompiler;
use DateTime;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
@ -158,17 +157,6 @@ final class CompiledPhpFileCacheTest extends TestCase
self::assertFalse($this->cache->deleteMultiple(['foo', 'bar']));
}
public function test_failing_validation_compilation_invalidates_cache_entry(): void
{
$compiler = new FakeCacheValidationCompiler();
$compiler->compileValidation = false;
$cache = new CompiledPhpFileCache('cache-dir', $compiler);
$cache->set('foo', 'foo');
self::assertFalse($cache->has('foo'));
}
public function test_clear_cache_does_not_delete_unrelated_files(): void
{
(vfsStream::newFile('some-unrelated-file.php'))->withContent('foo')->at($this->files);

View File

@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Cache\Compiled;
use CuyZ\Valinor\Cache\FileWatchingCache;
use CuyZ\Valinor\Tests\Fake\Cache\FakeCache;
use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition;
use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamContent;
use org\bovigo\vfs\vfsStreamDirectory;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use stdClass;
use function iterator_to_array;
final class FileWatchingCacheTest extends TestCase
{
private vfsStreamDirectory $files;
private FakeCache $delegateCache;
/** @var FileWatchingCache<mixed> */
private FileWatchingCache $cache;
protected function setUp(): void
{
parent::setUp();
$this->files = vfsStream::setup();
$this->delegateCache = new FakeCache();
$this->cache = new FileWatchingCache($this->delegateCache);
}
public function test_value_can_be_fetched_and_deleted(): void
{
$key = 'foo';
$value = new stdClass();
self::assertFalse($this->cache->has($key));
self::assertTrue($this->cache->set($key, $value));
self::assertTrue($this->cache->has($key));
self::assertSame($value, $this->cache->get($key));
self::assertTrue($this->cache->delete($key));
self::assertFalse($this->cache->has($key));
}
public function test_get_non_existing_entry_returns_default_value(): void
{
$defaultValue = new stdClass();
self::assertSame($defaultValue, $this->cache->get('Schwifty', $defaultValue));
}
public function test_get_existing_entry_does_not_return_default_value(): void
{
$this->cache->set('foo', 'foo');
self::assertSame('foo', $this->cache->get('foo', 'bar'));
}
public function test_clear_entries_clears_everything(): void
{
$keyA = 'foo';
$keyB = 'bar';
$this->cache->set($keyA, new stdClass());
$this->cache->set($keyB, new stdClass());
self::assertTrue($this->cache->has($keyA));
self::assertTrue($this->cache->has($keyB));
self::assertTrue($this->cache->clear());
self::assertFalse($this->cache->has($keyA));
self::assertFalse($this->cache->has($keyB));
}
public function test_multiple_values_set_can_be_fetched_and_deleted(): void
{
$values = [
'foo' => new stdClass(),
'bar' => new stdClass(),
];
self::assertFalse($this->cache->has('foo'));
self::assertFalse($this->cache->has('bar'));
self::assertTrue($this->cache->setMultiple($values));
self::assertTrue($this->cache->has('foo'));
self::assertTrue($this->cache->has('bar'));
// @PHP8.1 array unpacking
self::assertEquals($values, iterator_to_array($this->cache->getMultiple(['foo', 'bar']))); // @phpstan-ignore-line
self::assertTrue($this->cache->deleteMultiple(['foo', 'bar']));
self::assertFalse($this->cache->has('foo'));
self::assertFalse($this->cache->has('bar'));
}
public function test_set_php_internal_class_definition_saves_cache_entry(): void
{
$this->cache->set('some-class-definition', FakeClassDefinition::new(stdClass::class));
self::assertTrue($this->cache->has('some-class-definition'));
}
public function test_modifying_class_definition_file_invalids_cache(): void
{
$fileA = (vfsStream::newFile('ObjectA.php'))
->withContent('<?php class ObjectA {}')
->at($this->files);
$fileB = (vfsStream::newFile('ObjectB.php'))
->withContent('<?php class ObjectB extends ObjectA {}')
->at($this->files);
include $fileA->url();
include $fileB->url();
$class = FakeClassDefinition::fromReflection(new ReflectionClass('ObjectB')); // @phpstan-ignore-line
self::assertTrue($this->cache->set('some-class-definition', $class));
self::assertTrue($this->cache->has('some-class-definition'));
unlink($fileA->url());
$fileA->lastModified(time() + 5)->at($this->files);
self::assertFalse($this->cache->has('some-class-definition'));
self::assertTrue($this->cache->setMultiple(['some-class-definition' => $class]));
self::assertTrue($this->cache->has('some-class-definition'));
unlink($fileB->url());
self::assertFalse($this->cache->has('some-class-definition'));
}
public function test_modifying_function_definition_file_invalids_cache(): void
{
$file = $this->functionDefinitionFile();
$function = FakeFunctionDefinition::new($file->url());
self::assertTrue($this->cache->set('some-function-definition', $function));
self::assertTrue($this->cache->has('some-function-definition'));
unlink($file->url());
$file->lastModified(time() + 5)->at($this->files);
self::assertFalse($this->cache->has('some-function-definition'));
self::assertTrue($this->cache->setMultiple(['some-function-definition' => $function]));
self::assertTrue($this->cache->has('some-function-definition'));
unlink($file->url());
$file->lastModified(time() + 10)->at($this->files);
self::assertFalse($this->cache->has('some-function-definition'));
}
public function test_file_timestamps_are_fetched_once_per_request(): void
{
$cacheA = new FileWatchingCache($this->delegateCache);
$cacheB = new FileWatchingCache($this->delegateCache);
self::assertFalse($cacheA->has('some-function-definition'));
self::assertFalse($cacheA->has('some-function-definition'));
self::assertFalse($cacheB->has('some-function-definition'));
self::assertFalse($cacheB->has('some-function-definition'));
$file = $this->functionDefinitionFile()->url();
$cacheA->set('some-function-definition', FakeFunctionDefinition::new($file));
$cacheB->set('some-function-definition', FakeFunctionDefinition::new($file));
self::assertSame(2, $this->delegateCache->timesEntryWasSet('some-function-definition.timestamps'));
self::assertSame(2, $this->delegateCache->timesEntryWasFetched('some-function-definition.timestamps'));
}
private function functionDefinitionFile(): vfsStreamContent
{
return (vfsStream::newFile('_function_definition_file.php'))
->withContent('<?php function _valinor_test_function() {}')
->at($this->files);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Cache\Compiled;
use CuyZ\Valinor\Cache\Compiled\MixedValueCacheCompiler;
use PHPUnit\Framework\TestCase;
use stdClass;
final class MixedValueCacheCompilerTest extends TestCase
{
private MixedValueCacheCompiler $compiler;
protected function setUp(): void
{
parent::setUp();
$this->compiler = new MixedValueCacheCompiler();
}
/**
* @dataProvider values_are_compiled_correctly_data_provider
*
* @param mixed $value
*/
public function test_values_are_compiled_correctly($value): void
{
$compiledValue = eval('return ' . $this->compiler->compile($value) . ';');
self::assertEquals($value, $compiledValue);
}
public function values_are_compiled_correctly_data_provider(): iterable
{
yield 'Float' => [1337.42];
yield 'Int' => [404];
yield 'String' => ['Schwifty!'];
yield 'True' => [true];
yield 'False' => [false];
yield 'Array of scalar' => [[1337.42, 404, 'Schwifty!', true, false]];
yield 'Object' => [new stdClass()];
yield 'Array with object' => [[new stdClass()]];
}
}

View File

@ -21,6 +21,7 @@ final class FileSystemCacheTest extends TestCase
{
private vfsStreamDirectory $files;
/** @var FileSystemCache<mixed> */
private FileSystemCache $cache;
protected function setUp(): void
@ -139,6 +140,7 @@ final class FileSystemCacheTest extends TestCase
{
(function () use ($withFailingCache) {
$this->delegates = [
'*' => new FakeCache(),
ClassDefinition::class => new FakeCache(),
FunctionDefinition::class => $withFailingCache ? new FakeFailingCache() : new FakeCache(),
];

View File

@ -26,11 +26,4 @@ final class ClassDefinitionCompilerTest extends TestCase
$this->compiler->compile(new stdClass());
}
public function test_compile_validation_for_wrong_type_fails_assertion(): void
{
$this->expectException(AssertionError::class);
$this->compiler->compileValidation(new stdClass());
}
}