allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

26
vendor/cuyz/valinor/src/Cache/Cache.php vendored Normal file
View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
/**
* @internal
*
* @template T = mixed
*/
interface Cache
{
/**
* @param non-empty-string $key
* @return null|T
*/
public function get(string $key, mixed ...$arguments): mixed;
/**
* @param non-empty-string $key
*/
public function set(string $key, CacheEntry $entry): void;
public function clear(): void;
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
/** @internal */
final class CacheEntry
{
public function __construct(
public readonly string $code,
/** @var list<non-empty-string> */
public readonly array $filesToWatch = [],
) {}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Exception;
use RuntimeException;
/** @internal */
final class CacheDirectoryNotWritable extends RuntimeException
{
public function __construct(string $directory)
{
parent::__construct("Provided directory `$directory` is not writable.");
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Exception;
use RuntimeException;
/** @internal */
final class CompiledPhpCacheFileNotWritten extends RuntimeException
{
public function __construct(string $file)
{
parent::__construct("File `$file` could not be written.");
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Exception;
use RuntimeException;
/** @internal */
final class CorruptedCompiledPhpCacheFile extends RuntimeException
{
public function __construct(string $filename)
{
parent::__construct("Compiled php cache file `$filename` has corrupted value.");
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Exception;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use RuntimeException;
use function lcfirst;
/** @internal */
final class InvalidSignatureToWarmup extends RuntimeException
{
public function __construct(UnresolvableType $unresolvableType)
{
parent::__construct(
"Cannot warm up invalid signature `{$unresolvableType->toString()}`: " . lcfirst($unresolvableType->message()),
);
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use CuyZ\Valinor\Cache\Exception\CacheDirectoryNotWritable;
use CuyZ\Valinor\Cache\Exception\CompiledPhpCacheFileNotWritten;
use CuyZ\Valinor\Cache\Exception\CorruptedCompiledPhpCacheFile;
use Error;
use FilesystemIterator;
use function bin2hex;
use function file_exists;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function random_bytes;
use function rename;
use function rmdir;
use function str_contains;
use function umask;
use function unlink;
/**
* @api
*
* @template EntryType
* @implements Cache<EntryType>
*/
final class FileSystemCache implements Cache
{
private const GENERATED_MESSAGE = 'Generated by ' . self::class;
public function __construct(
private string $cacheDir,
) {}
/** @internal */
public function get(string $key, mixed ...$arguments): mixed
{
$filename = $this->cacheDir . DIRECTORY_SEPARATOR . $key . '.php';
if (! file_exists($filename)) {
return null;
}
try {
return (require $filename)(...$arguments); // @phpstan-ignore callable.nonCallable
} catch (Error) {
throw new CorruptedCompiledPhpCacheFile($filename);
}
}
/** @internal */
public function set(string $key, CacheEntry $entry): void
{
$tmpDir = $this->cacheDir . DIRECTORY_SEPARATOR . '.valinor.tmp';
$filename = $this->cacheDir . DIRECTORY_SEPARATOR . $key . '.php';
if (! is_dir($tmpDir) && ! @mkdir($tmpDir, 0777, true)) {
throw new CacheDirectoryNotWritable($this->cacheDir);
}
/** @infection-ignore-all */
$tmpFilename = $tmpDir . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16));
try {
$code = '<?php // ' . self::GENERATED_MESSAGE . PHP_EOL . "return $entry->code;" . PHP_EOL;
if (! @file_put_contents($tmpFilename, $code)) {
throw new CompiledPhpCacheFileNotWritten($tmpFilename);
}
if (! @rename($tmpFilename, $filename)) {
throw new CompiledPhpCacheFileNotWritten($filename);
}
@chmod($filename, 0666 & ~umask());
} finally {
@unlink($tmpFilename);
}
}
public function clear(): void
{
if (! is_dir($this->cacheDir)) {
return;
}
$shouldDeleteRootDir = true;
/** @var FilesystemIterator $file */
foreach (new FilesystemIterator($this->cacheDir) as $file) {
if ($file->getFilename() === '.valinor.tmp') {
@rmdir($file->getPathname());
continue;
}
if (! $file->isFile()) {
$shouldDeleteRootDir = false;
continue;
}
$line = $file->openFile()->getCurrentLine();
if (! $line || ! str_contains($line, self::GENERATED_MESSAGE)) {
$shouldDeleteRootDir = false;
continue;
}
@unlink($file->getPathname());
}
if ($shouldDeleteRootDir) {
@rmdir($this->cacheDir);
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use function file_exists;
use function filemtime;
use function var_export;
/**
* 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
*
* @template EntryType
* @implements Cache<EntryType>
*/
final class FileWatchingCache implements Cache
{
/** @var array<string, array<string, int>> */
private array $timestamps = [];
public function __construct(
/** @var Cache<EntryType> */
private Cache $delegate,
) {}
/** @internal */
public function get(string $key, mixed ...$arguments): mixed
{
$this->timestamps[$key] ??= $this->delegate->get("$key.timestamps"); // @phpstan-ignore assign.propertyType
if ($this->timestamps[$key] === null) {
return null;
}
assert(is_array($this->timestamps[$key]));
foreach ($this->timestamps[$key] as $fileName => $timestamp) {
if (! file_exists($fileName)) {
return null;
}
if (filemtime($fileName) !== $timestamp) {
return null;
}
}
return $this->delegate->get($key, ...$arguments);
}
/** @internal */
public function set(string $key, CacheEntry $entry): void
{
$this->delegate->set($key, $entry);
$this->timestamps[$key] = [];
foreach ($entry->filesToWatch as $fileName) {
$time = @filemtime($fileName);
if (false === $time) {
continue;
}
$this->timestamps[$key][$fileName] = $time;
}
$code = 'fn () => ' . var_export($this->timestamps[$key], true);
$this->delegate->set("$key.timestamps", new CacheEntry($code));
}
public function clear(): void
{
$this->timestamps = [];
$this->delegate->clear();
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use CuyZ\Valinor\Library\Settings;
use CuyZ\Valinor\Utility\Package;
use function hash;
use function strstr;
/**
* @internal
*
* @template EntryType
* @implements Cache<EntryType>
*/
final class KeySanitizerCache implements Cache
{
private static string $version;
public function __construct(
/** @var Cache<EntryType> */
private Cache $delegate,
private Settings $settings,
) {}
public function get(string $key, mixed ...$arguments): mixed
{
return $this->delegate->get($this->sanitize($key), ...$arguments);
}
public function set(string $key, CacheEntry $entry): void
{
$this->delegate->set($this->sanitize($key), $entry);
}
public function clear(): void
{
$this->delegate->clear();
}
/**
* @return non-empty-string
*/
private function sanitize(string $key): string
{
// @infection-ignore-all
self::$version ??= PHP_VERSION . '/' . Package::version();
$firstPart = strstr($key, "\0", before_needle: true);
// @infection-ignore-all
return $firstPart . hash('xxh128', $key . $this->settings->hash() . self::$version);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
/**
* @internal
*
* @template EntryType
* @implements Cache<EntryType>
*/
final class RuntimeCache implements Cache
{
/** @var array<string, EntryType|null> */
private array $entries = [];
public function __construct(
/** @var Cache<EntryType> */
private Cache $delegate,
) {}
public function get(string $key, mixed ...$arguments): mixed
{
return $this->entries[$key] ??= $this->delegate->get($key, ...$arguments);
}
public function set(string $key, CacheEntry $entry): void
{
$this->delegate->set($key, $entry);
}
public function clear(): void
{
$this->entries = [];
$this->delegate->clear();
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use Closure;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Library\Settings;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use CuyZ\Valinor\Utility\TypeHelper;
use ReflectionFunction;
use function array_filter;
use function array_unique;
use function array_values;
/** @internal */
final class TypeFilesWatcher
{
public function __construct(
private Settings $settings,
private ClassDefinitionRepository $classDefinitionRepository,
) {}
/**
* This method returns a list of files in which are declared all types that
* compose the given type. If the given type is composed of other types,
* they are recursively resolved as well. If the type is a class, all
* properties are also resolved.
*
* In addition, all callable that were given to the global settings are
* checked, and their file names are added to the list.
*
* This file list can then be used by the `FileWatchingCache` to detect
* changes to these files and invalidate the cache entries if needed.
*
* Example of returned value:
*
* ```php
* [
* // An entity representing a user
* '/root/path/src/Domain/User/User.php',
*
* // The user has an `$email` property and a `$phoneNumber` property
* '/root/path/src/Domain/Shared/Email.php',
* '/root/path/src/Domain/Shared/PhoneNumber.php',
*
* // The settings contain a custom constructor defined in a file
* '/root/path/src/Infrastructure/Mapper/CustomConstructor.php',
*
* // The settings contain a custom exception filter
* '/root/path/src/Infrastructure/Mapper/CustomExceptionFilter.php',
*
* // And maybe more…
* ];
* ```
*
* @return list<non-empty-string>
*/
public function for(Type $type): array
{
// Merging the files bound to settings and the files bound to the type
$files = [
...array_map(
fn (callable $callable) => (new ReflectionFunction(Closure::fromCallable($callable)))->getFileName(),
$this->settings->callables(),
),
...$this->filesToWatch($type),
];
// Removing the duplicates
$files = array_unique($files);
// Filtering the empty/invalid file names
$files = array_filter($files, fn ($value) => is_string($value));
/** @var list<non-empty-string> */
return array_values($files);
}
/**
* @param array<non-empty-string> $files
* @return array<non-empty-string>
*/
private function filesToWatch(Type $type, array $files = []): array
{
if (isset($files[$type->toString()])) {
// Prevents infinite loop in case of circular references
return [];
}
foreach (TypeHelper::traverseRecursively($type) as $subType) {
$files = [...$files, ...$this->filesToWatch($subType, $files)];
}
if ($type instanceof ObjectType) {
$fileName = Reflection::class($type->className())->getFileName();
if (! $fileName) {
return [];
}
$files[$type->toString()] = $fileName;
$class = $this->classDefinitionRepository->for($type);
foreach ($class->properties as $property) {
$files = [...$files, ...$this->filesToWatch($property->type, $files)];
}
}
return $files;
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Warmup;
use CuyZ\Valinor\Cache\Exception\InvalidSignatureToWarmup;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Tree\Builder\InterfaceInferringContainer;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\Parser\TypeParser;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\InterfaceType;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use CuyZ\Valinor\Utility\TypeHelper;
use function in_array;
/** @internal */
final class RecursiveCacheWarmupService
{
/** @var list<class-string> */
private array $classesWarmedUp = [];
public function __construct(
private TypeParser $parser,
private InterfaceInferringContainer $interfaceInferringContainer,
private ClassDefinitionRepository $classDefinitionRepository,
private ObjectBuilderFactory $objectBuilderFactory
) {}
public function warmup(string ...$signatures): void
{
foreach ($signatures as $signature) {
$type = $this->parser->parse($signature);
if ($type instanceof UnresolvableType) {
throw new InvalidSignatureToWarmup($type);
}
$this->warmupType($type);
}
}
private function warmupType(Type $type): void
{
if ($type instanceof InterfaceType) {
$this->warmupInterfaceType($type);
}
if ($type instanceof ClassType) {
$this->warmupClassType($type);
}
foreach (TypeHelper::traverseRecursively($type) as $subType) {
$this->warmupType($subType);
}
}
private function warmupInterfaceType(InterfaceType $type): void
{
$interfaceName = $type->className();
if (! $this->interfaceInferringContainer->has($interfaceName)) {
return;
}
$function = $this->interfaceInferringContainer->inferFunctionFor($interfaceName);
$this->warmupType($function->returnType);
foreach ($function->parameters as $parameter) {
$this->warmupType($parameter->type);
}
}
private function warmupClassType(ClassType $type): void
{
if (in_array($type->className(), $this->classesWarmedUp, true)) {
return;
}
$this->classesWarmedUp[] = $type->className();
$classDefinition = $this->classDefinitionRepository->for($type);
$objectBuilders = $this->objectBuilderFactory->for($classDefinition);
foreach ($objectBuilders as $builder) {
foreach ($builder->describeArguments() as $argument) {
$this->warmupType($argument->type());
}
}
}
}