mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
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:
parent
69ad3f4777
commit
2d70efbfbb
10
README.md
10
README.md
@ -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()
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
16
src/Cache/Compiled/MixedValueCacheCompiler.php
Normal file
16
src/Cache/Compiled/MixedValueCacheCompiler.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
152
src/Cache/FileWatchingCache.php
Normal file
152
src/Cache/FileWatchingCache.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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);
|
||||
|
189
tests/Unit/Cache/Compiled/FileWatchingCacheTest.php
Normal file
189
tests/Unit/Cache/Compiled/FileWatchingCacheTest.php
Normal 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);
|
||||
}
|
||||
}
|
45
tests/Unit/Cache/Compiled/MixedValueCacheCompilerTest.php
Normal file
45
tests/Unit/Cache/Compiled/MixedValueCacheCompilerTest.php
Normal 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()]];
|
||||
}
|
||||
}
|
@ -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(),
|
||||
];
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user