update lock clucknut
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / build-and-push (push) Successful in 3m14s
Build, Push and Deploy / deploy-staging (push) Successful in 25s
Build, Push and Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-18 20:32:18 +07:00
parent 4554035227
commit dcaf267458
3359 changed files with 153185 additions and 205489 deletions

View File

@@ -5,11 +5,11 @@
namespace CuyZ\Valinor\Cache;
/** @internal */
final class CacheEntry
final readonly class CacheEntry
{
public function __construct(
public readonly string $code,
/** @var list<non-empty-string> */
public readonly array $filesToWatch = [],
public string $code,
/** @var null|callable(): list<non-empty-string> */
public mixed $filesToWatch = null,
) {}
}

View File

@@ -12,6 +12,7 @@
use FilesystemIterator;
use function bin2hex;
use function chmod;
use function file_exists;
use function file_put_contents;
use function is_dir;
@@ -59,7 +60,14 @@ 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)) {
/**
* The third `is_dir()` check confirms the directory truly doesn't exist
* after `mkdir()` failure, distinguishing a genuine error (permissions,
* disk full) from a concurrent creation by another process.
*
* @infection-ignore-all
*/
if (! is_dir($tmpDir) && ! @mkdir($tmpDir, 0777, true) && ! is_dir($tmpDir)) {
throw new CacheDirectoryNotWritable($this->cacheDir);
}
@@ -79,7 +87,9 @@ public function set(string $key, CacheEntry $entry): void
@chmod($filename, 0666 & ~umask());
} finally {
@unlink($tmpFilename);
if (file_exists($tmpFilename)) {
unlink($tmpFilename);
}
}
}

View File

@@ -4,8 +4,10 @@
namespace CuyZ\Valinor\Cache;
use function assert;
use function file_exists;
use function filemtime;
use function is_array;
use function var_export;
/**
@@ -18,7 +20,7 @@
* 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
* the mapper builder: {@see \CuyZ\Valinor\MapperBuilder::withCache()}
*
* @api
*
@@ -66,7 +68,11 @@ public function set(string $key, CacheEntry $entry): void
$this->timestamps[$key] = [];
foreach ($entry->filesToWatch as $fileName) {
if ($entry->filesToWatch === null) {
return;
}
foreach (($entry->filesToWatch)() as $fileName) {
$time = @filemtime($fileName);
if (false === $time) {

View File

@@ -18,8 +18,6 @@
*/
final class KeySanitizerCache implements Cache
{
private static string $version;
public function __construct(
/** @var Cache<EntryType> */
private Cache $delegate,
@@ -46,12 +44,10 @@ public function clear(): void
*/
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);
$hash = hash('xxh128', $key . $this->settings->hash() . PHP_VERSION . Package::VERSION);
return $firstPart . $hash;
}
}

View File

@@ -4,18 +4,22 @@
namespace CuyZ\Valinor\Cache;
use Closure;
use CuyZ\Valinor\Definition\Attributes;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
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_map;
use function array_merge;
use function array_unique;
use function array_values;
use function is_string;
/** @internal */
final class TypeFilesWatcher
@@ -23,23 +27,33 @@ final class TypeFilesWatcher
public function __construct(
private Settings $settings,
private ClassDefinitionRepository $classDefinitionRepository,
private FunctionDefinitionRepository $functionDefinitionRepository,
) {}
/**
* 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.
* they are recursively resolved as well.
*
* 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:
* If the type is a class, will be resolved:
* - The class attributes
* - The properties attributes and types
* - The methods attributes and return types
* - The method parameters attributes and types
*
* ```php
* If the type is a callable, will be resolved:
* - The callable attributes and return type
* - The callable parameters attributes and types
*
* In addition, all callables that were given to the global settings are
* checked, and their file names are added to the list.
*
* Example of a returned value:
*
* ```
* [
* // An entity representing a user
* '/root/path/src/Domain/User/User.php',
@@ -60,59 +74,156 @@ public function __construct(
*
* @return list<non-empty-string>
*/
public function for(Type $type): array
public function for(Type|callable $item): 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),
];
$files = array_merge(...array_map(
$this->forCallable(...),
$this->settings->callables(),
));
if ($item instanceof Type) {
$files = [...$files, ...$this->forType($item)];
} else {
$files = [...$files, ...$this->forCallable($item)];
}
// Removing the duplicates
$files = array_unique($files);
// Filtering the empty/invalid file names
$files = array_filter($files, fn ($value) => is_string($value));
$files = array_filter($files, is_string(...));
/** @var list<non-empty-string> */
return array_values($files);
}
/**
* @param array<non-empty-string> $files
* @return array<non-empty-string>
* @return list<non-empty-string>
*/
private function filesToWatch(Type $type, array $files = []): array
private function forCallable(callable $callable): array
{
if (isset($files[$type->toString()])) {
$definition = $this->functionDefinitionRepository->for($callable);
$visited = [];
$files = [];
if ($definition->fileName && $definition->class !== Settings::class) {
$files = [$definition->fileName];
}
$returnFiles = $this->forType($definition->returnType, $visited);
$attributeFiles = $this->forAttributes($definition->attributes, $visited);
$files = [...$files, ...$returnFiles, ...$attributeFiles];
foreach ($definition->parameters as $parameter) {
$parameterFiles = $this->forType($parameter->type, $visited);
$attributeFiles = $this->forAttributes($parameter->attributes, $visited);
$files = [...$files, ...$parameterFiles, ...$attributeFiles];
}
return $files;
}
/**
* @param array<string, true> $visited
* @return list<non-empty-string>
*/
private function forType(Type $type, array &$visited = []): array
{
if (isset($visited[$type->toString()])) {
// Prevents infinite loop in case of circular references
return [];
}
$visited[$type->toString()] = true;
$files = [];
foreach (TypeHelper::traverseRecursively($type) as $subType) {
$files = [...$files, ...$this->filesToWatch($subType, $files)];
$files = [...$files, ...$this->forType($subType, $visited)];
}
if ($type instanceof ObjectType) {
$fileName = Reflection::class($type->className())->getFileName();
if (! $fileName) {
return [];
}
$files[$type->toString()] = $fileName;
$class = $this->classDefinitionRepository->for($type);
$classFiles = $this->forClass($class, $visited);
foreach ($class->properties as $property) {
$files = [...$files, ...$this->filesToWatch($property->type, $files)];
$files = [...$files, ...$classFiles];
}
return $files;
}
/**
* @param array<string, true> $visited
* @return list<non-empty-string>
*/
private function forClass(ClassDefinition $class, array &$visited): array
{
$objectFiles = $this->traverseObjectInheritanceFileNames($class->name);
$attributeFiles = $this->forAttributes($class->attributes, $visited);
$files = [...$objectFiles, ...$attributeFiles];
foreach ($class->properties as $property) {
$propertyFiles = $this->forType($property->type, $visited);
$attributeFiles = $this->forAttributes($property->attributes, $visited);
$files = [...$files, ...$propertyFiles, ...$attributeFiles];
}
foreach ($class->methods as $method) {
$returnFiles = $this->forType($method->returnType, $visited);
$attributeFiles = $this->forAttributes($method->attributes, $visited);
$files = [...$files, ...$returnFiles, ...$attributeFiles];
foreach ($method->parameters as $parameter) {
$parameterFiles = $this->forType($parameter->type, $visited);
$attributeFiles = $this->forAttributes($parameter->attributes, $visited);
$files = [...$files, ...$parameterFiles, ...$attributeFiles];
}
}
return $files;
}
/**
* @param array<string, true> $visited
* @return list<non-empty-string>
*/
private function forAttributes(Attributes $attributes, array &$visited): array
{
$files = [];
foreach ($attributes as $attribute) {
$files = [...$files, ...$this->forType($attribute->class->type, $visited)];
}
return $files;
}
/**
* @param class-string $className
* @return list<non-empty-string>
*/
private function traverseObjectInheritanceFileNames(string $className): array
{
$reflection = Reflection::class($className);
$fileNames = [];
do {
$fileName = $reflection->getFileName();
if (is_string($fileName)) {
$fileNames[] = $fileName;
}
} while ($reflection = $reflection->getParentClass());
return $fileNames;
}
}