LICENSE.md 0000644 00000002040 14350444705 0006152 0 ustar 00 Copyright (c) 2021 Romain Canon
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
README.md 0000644 00000006360 14350444705 0006036 0 ustar 00
![Valinor banner](docs/pages/img/valinor-banner.svg)
— From boring old arrays to shiny typed objects —
[![Latest Stable Version](https://poser.pugx.org/cuyz/valinor/v)][link-packagist]
[![PHP Version Require](https://poser.pugx.org/cuyz/valinor/require/php)][link-packagist]
[![Total Downloads](https://poser.pugx.org/cuyz/valinor/downloads)][link-packagist]
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FCuyZ%2FValinor%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/CuyZ/Valinor/master)
---
Valinor takes care of the construction and validation of raw inputs (JSON, plain
arrays, etc.) into objects, ensuring a perfectly valid state. It allows the
objects to be used without having to worry about their integrity during the
whole application lifecycle.
The validation system will detect any incorrect value and help the developers by
providing precise and human-readable error messages.
The mapper can handle native PHP types as well as other advanced types supported
by [PHPStan] and [Psalm] like shaped arrays, generics, integer ranges and more.
## Installation
```bash
composer require cuyz/valinor
```
**📔 Read more on the [online documentation](https://valinor.cuyz.io)**
## Example
```php
final class Country
{
public function __construct(
/** @var non-empty-string */
public readonly string $name,
/** @var list */
public readonly array $cities,
) {}
}
final class City
{
public function __construct(
/** @var non-empty-string */
public readonly string $name,
public readonly DateTimeZone $timeZone,
) {}
}
$json = <<mapper()
->map(Country::class, \CuyZ\Valinor\Mapper\Source\Source::json($json));
echo $country->name; // France
echo $country->cities[0]->name; // Paris
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
// Handle the error…
}
```
## Documentation
The full documentation is available on [valinor.cuyz.io].
## Credits & thank you
The development of this library is mainly motivated by the kind words and the
help of many people. I am grateful to everyone, especially to the [contributors]
of this repository who directly help to push the project forward.
I also want to thank
[![blackfire-logo] Blackfire](https://www.blackfire.io/?utm_source=valinor&utm_medium=readme&utm_campaign=free-open-source)
for providing a license of their awesome tool, leading to notable performance
gains when using this library.
[link-packagist]: https://packagist.org/packages/cuyz/valinor
[contributors]: https://github.com/CuyZ/Valinor/graphs/contributors
[PHPStan]: https://phpstan.org/
[Psalm]: https://psalm.dev/
[Blackfire]: https://www.blackfire.io/?utm_source=valinor&utm_medium=readme&utm_campaign=free-open-source
[blackfire-logo]: docs/pages/img/blackfire-logo.svg "Blackfire logo"
[valinor.cuyz.io]: https://valinor.cuyz.io
composer.json 0000644 00000004744 14350444705 0007305 0 ustar 00 {
"name": "cuyz/valinor",
"type": "library",
"description": "Library that helps to map any input into a strongly-typed value object structure.",
"keywords": [
"object", "tree", "mapper", "mapping", "hydrator", "array", "conversion", "json", "yaml"
],
"homepage": "https://github.com/CuyZ/Valinor",
"license": "MIT",
"authors": [
{
"name": "Romain Canon",
"email": "romain.hydrocanon@gmail.com",
"homepage": "https://github.com/romm"
}
],
"require": {
"php": "~8.0.0 || ~8.1.0 || ~8.2.0",
"composer-runtime-api": "^2.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"infection/infection": "^0.26",
"phpstan/phpstan": "^1.3",
"phpstan/phpstan-strict-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"friendsofphp/php-cs-fixer": "^3.4",
"marcocesarato/php-conventional-changelog": "^1.12",
"vimeo/psalm": "^5.0",
"mikey179/vfsstream": "^1.6.10",
"rector/rector": "^0.12.23"
},
"autoload": {
"psr-4": {
"CuyZ\\Valinor\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"CuyZ\\Valinor\\Tests\\": "tests",
"CuyZ\\Valinor\\QA\\": "qa"
}
},
"scripts": {
"check": [
"@putenv XDEBUG_MODE=off",
"phpunit",
"psalm --config=tests/StaticAnalysis/psalm-without-plugin.xml",
"psalm --config=tests/StaticAnalysis/psalm-with-plugin.xml",
"phpstan --configuration=tests/StaticAnalysis/phpstan-without-extension.neon.dist",
"phpstan --configuration=tests/StaticAnalysis/phpstan-with-extension.neon.dist",
"phpstan",
"@putenv PHP_CS_FIXER_IGNORE_ENV=1",
"php-cs-fixer fix --dry-run",
"rector --dry-run"
],
"fix": [
"@putenv XDEBUG_MODE=off",
"@putenv PHP_CS_FIXER_IGNORE_ENV=1",
"php-cs-fixer fix",
"rector"
],
"mutation": [
"infection --threads=max --git-diff-lines"
],
"doc": [
"Composer\\Config::disableProcessTimeout",
"pip install -r docs/requirements.txt",
"mkdocs serve --config-file docs/mkdocs.yml"
]
},
"config": {
"allow-plugins": {
"infection/extension-installer": false
}
}
}
qa/PHPStan/Extension/ApiAndInternalAnnotationCheck.php 0000644 00000002333 14350444705 0016740 0 ustar 00
*/
final class ApiAndInternalAnnotationCheck implements Rule
{
public function getNodeType(): string
{
return InClassNode::class;
}
public function processNode(Node $node, Scope $scope): array
{
$reflection = $scope->getClassReflection();
if (! $reflection) {
return [];
}
if ($reflection->isAnonymous()) {
return [];
}
if (str_starts_with($reflection->getName(), 'CuyZ\Valinor\Tests')) {
return [];
}
if (str_starts_with($reflection->getName(), 'CuyZ\Valinor\QA')) {
return [];
}
if (! preg_match('/@(api|internal)\s+/', $reflection->getResolvedPhpDoc()?->getPhpDocString() ?? '')) {
return [
RuleErrorBuilder::message(
'Missing annotation `@api` or `@internal`.'
)->build(),
];
}
return [];
}
}
qa/PHPStan/Extension/ArgumentsMapperPHPStanExtension.php 0000644 00000003600 14350444705 0017321 0 ustar 00 getName() === 'mapArguments';
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$arguments = $methodCall->getArgs();
if (count($arguments) === 0) {
return new MixedType();
}
$type = $scope->getType($arguments[0]->value);
if ($type instanceof ClosureType) {
$parameters = $type->getParameters();
} elseif ($type instanceof ConstantArrayType) {
$acceptors = $type->getCallableParametersAcceptors($scope);
if (count($acceptors) !== 1) {
return new MixedType();
}
$parameters = $acceptors[0]->getParameters();
} else {
return new MixedType();
}
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($parameters as $parameter) {
$builder->setOffsetValueType(new ConstantStringType($parameter->getName()), $parameter->getType(), $parameter->isOptional());
}
return $builder->getArray();
}
}
qa/PHPStan/Extension/TreeMapperPHPStanExtension.php 0000644 00000003532 14350444705 0016257 0 ustar 00 getName() === 'map';
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$arguments = $methodCall->getArgs();
if (count($arguments) === 0) {
return new MixedType();
}
$type = $scope->getType($arguments[0]->value);
if ($type instanceof UnionType) {
return $type->traverse(fn (Type $type) => $this->type($type));
}
return $this->type($type);
}
private function type(Type $type): Type
{
if ($type instanceof GenericClassStringType) {
return $type->getGenericType();
}
if ($type instanceof ConstantStringType) {
return $this->resolver->resolve($type->getValue());
}
if ($type instanceof ClassStringType) {
return new ObjectWithoutClassType();
}
return new MixedType();
}
}
qa/PHPStan/Stubs/Psr/SimpleCache/CacheInterface.stub 0000644 00000001754 14350444705 0016203 0 ustar 00 $keys
* @param EntryType $default
* @return iterable
*/
public function getMultiple(iterable $keys, $default = null): iterable;
/**
* @param iterable $values
* @param null|int|DateInterval $ttl
*/
public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool;
/**
* @param iterable $keys
*/
public function deleteMultiple(iterable $keys): bool;
}
qa/PHPStan/valinor-phpstan-configuration.php 0000644 00000001137 14350444705 0015135 0 ustar 00 [
[
'class' => TreeMapperPHPStanExtension::class,
'tags' => ['phpstan.broker.dynamicMethodReturnTypeExtension']
], [
'class' => ArgumentsMapperPHPStanExtension::class,
'tags' => ['phpstan.broker.dynamicMethodReturnTypeExtension']
],
],
];
qa/Psalm/Plugin/ArgumentsMapperPsalmPlugin.php 0000644 00000003573 14350444705 0015474 0 ustar 00 getMethodNameLowercase() !== 'maparguments') {
return null;
}
$arguments = $event->getCallArgs();
if (count($arguments) === 0) {
return null;
}
$types = $event->getSource()->getNodeTypeProvider()->getType($arguments[0]->value);
if ($types === null) {
return null;
}
$types = $types->getAtomicTypes();
if (count($types) !== 1) {
return null;
}
$type = reset($types);
if ($type instanceof TKeyedArray) {
// Internal class usage, see https://github.com/vimeo/psalm/issues/8726
$type = CallableTypeComparator::getCallableFromAtomic($event->getSource()->getCodebase(), $type);
}
if (! $type instanceof TClosure) {
return null;
}
if (empty($type->params ?? [])) {
return null;
}
$params = [];
foreach ($type->params as $param) {
$params[$param->name] = $param->type ?? new Union([new TMixed()]);
}
return new Union([new TKeyedArray($params)]);
}
}
qa/Psalm/Plugin/TreeMapperPsalmPlugin.php 0000644 00000003373 14350444705 0014424 0 ustar 00 getMethodNameLowercase() !== 'map') {
return null;
}
$arguments = $event->getCallArgs();
if (count($arguments) === 0) {
return null;
}
$type = $event->getSource()->getNodeTypeProvider()->getType($arguments[0]->value);
if (! $type) {
return null;
}
$types = [];
foreach ($type->getAtomicTypes() as $node) {
$inferred = self::type($node);
if ($inferred === null) {
return null;
}
$types[] = $inferred;
}
return Type::combineUnionTypeArray($types, $event->getSource()->getCodebase());
}
private static function type(Atomic $node): ?Union
{
return match (true) {
$node instanceof TLiteralString => Type::parseString($node->value),
$node instanceof TDependentGetClass => $node->as_type,
$node instanceof TClassString && $node->as_type => new Union([$node->as_type]),
default => null,
};
}
}
qa/Psalm/ValinorPsalmPlugin.php 0000644 00000001322 14350444705 0012524 0 ustar 00 registerHooksFromClass(TreeMapperPsalmPlugin::class);
$api->registerHooksFromClass(ArgumentsMapperPsalmPlugin::class);
}
}
src/Cache/ChainCache.php 0000644 00000005077 14350444705 0011034 0 ustar 00
*/
final class ChainCache implements CacheInterface
{
/** @var array> */
private array $delegates;
private int $count;
/**
* @param CacheInterface ...$delegates
*/
public function __construct(CacheInterface ...$delegates)
{
$this->delegates = $delegates;
$this->count = count($delegates);
}
public function get($key, $default = null): mixed
{
foreach ($this->delegates as $i => $delegate) {
$value = $delegate->get($key, $default);
if (null !== $value) {
while (--$i >= 0) {
$this->delegates[$i]->set($key, $value);
}
return $value;
}
}
return $default;
}
public function set($key, $value, $ttl = null): bool
{
$saved = true;
$i = $this->count;
while ($i--) {
$saved = $this->delegates[$i]->set($key, $value, $ttl) && $saved;
}
return $saved;
}
public function delete($key): bool
{
$deleted = true;
$i = $this->count;
while ($i--) {
$deleted = $this->delegates[$i]->delete($key) && $deleted;
}
return $deleted;
}
public function clear(): bool
{
$cleared = true;
$i = $this->count;
while ($i--) {
$cleared = $this->delegates[$i]->clear() && $cleared;
}
return $cleared;
}
/**
* @return Traversable
*/
public function getMultiple($keys, $default = null): Traversable
{
foreach ($keys as $key) {
yield $key => $this->get($key, $default);
}
}
public function setMultiple($values, $ttl = null): bool
{
$saved = true;
foreach ($values as $key => $value) {
$saved = $this->set($key, $value, $ttl) && $saved;
}
return $saved;
}
public function deleteMultiple($keys): bool
{
$deleted = true;
foreach ($keys as $key) {
$deleted = $this->delete($key) && $deleted;
}
return $deleted;
}
public function has($key): bool
{
foreach ($this->delegates as $cache) {
if ($cache->has($key)) {
return true;
}
}
return false;
}
}
src/Cache/Compiled/CacheCompiler.php 0000644 00000000251 14350444705 0013305 0 ustar 00
*/
final class CompiledPhpFileCache implements CacheInterface
{
private const TEMPORARY_DIR_PERMISSION = 510;
private const GENERATED_MESSAGE = 'Generated by ' . self::class;
/** @var array> */
private array $files = [];
public function __construct(
private string $cacheDir,
private CacheCompiler $compiler
) {
}
public function has($key): bool
{
$filename = $this->path($key);
if (! file_exists($filename)) {
return false;
}
return $this->getFile($filename)->isValid();
}
public function get($key, $default = null): mixed
{
if (! $this->has($key)) {
return $default;
}
$filename = $this->path($key);
return $this->getFile($filename)->value();
}
public function set($key, $value, $ttl = null): bool
{
$filename = $this->path($key);
assert(! file_exists($filename));
$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);
}
/** @infection-ignore-all */
$tmpFilename = $tmpDir . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16));
try {
if (! @file_put_contents($tmpFilename, $code)) {
throw new CompiledPhpCacheFileNotWritten($tmpFilename);
}
if (! file_exists($filename) && ! @rename($tmpFilename, $filename)) {
throw new CompiledPhpCacheFileNotWritten($filename);
}
} finally {
@unlink($tmpFilename);
}
return true;
}
public function delete($key): bool
{
$filename = $this->path($key);
if (file_exists($filename)) {
return @unlink($filename);
}
return true;
}
public function clear(): bool
{
$success = true;
/** @var FilesystemIterator $file */
foreach (new FilesystemIterator($this->cacheDir) as $file) {
if (! $file->isFile()) {
continue;
}
$line = $file->openFile()->getCurrentLine();
if (! $line || ! str_contains($line, self::GENERATED_MESSAGE)) {
continue;
}
$success = @unlink($this->cacheDir . DIRECTORY_SEPARATOR . $file->getFilename()) && $success;
}
return $success;
}
/**
* @return Traversable
*/
public function getMultiple($keys, $default = null): Traversable
{
foreach ($keys as $key) {
yield $key => $this->get($key, $default);
}
}
public function setMultiple($values, $ttl = null): bool
{
foreach ($values as $key => $value) {
$this->set($key, $value, $ttl);
}
return true;
}
public function deleteMultiple($keys): bool
{
$deleted = true;
foreach ($keys as $key) {
$deleted = $this->delete($key) && $deleted;
}
return $deleted;
}
private function compile(mixed $value, int|DateInterval|null $ttl = null): string
{
$validationCode = 'true';
if ($ttl) {
$time = $ttl instanceof DateInterval
? (new DateTime())->add($ttl)->getTimestamp()
: time() + $ttl;
$validationCode = "time() < $time";
}
$generatedMessage = self::GENERATED_MESSAGE;
$code = $this->compiler->compile($value);
return <<compiler instanceof \CuyZ\Valinor\Cache\Compiled\HasArguments ? \$this->compiler->arguments() : []) implements \CuyZ\Valinor\Cache\Compiled\PhpCacheFile {
/** @var array */
private array \$arguments;
public function __construct(array \$arguments)
{
\$this->arguments = \$arguments;
}
public function value()
{
return $code;
}
public function isValid(): bool
{
return $validationCode;
}
};
PHP;
}
/**
* @return PhpCacheFile
*/
private function getFile(string $filename): PhpCacheFile
{
if (! isset($this->files[$filename])) {
try {
$object = include $filename;
} catch (Error) {
}
if (! isset($object) || ! $object instanceof PhpCacheFile) {
throw new CorruptedCompiledPhpCacheFile($filename);
}
$this->files[$filename] = $object;
}
return $this->files[$filename];
}
private function path(string $key): string
{
/** @infection-ignore-all */
return $this->cacheDir . DIRECTORY_SEPARATOR . sha1($key) . '.php';
}
}
src/Cache/Compiled/HasArguments.php 0000644 00000000347 14350444705 0013216 0 ustar 00
*/
public function arguments(): array;
}
src/Cache/Compiled/MixedValueCacheCompiler.php 0000644 00000000434 14350444705 0015274 0 ustar 00 getMessage()}",
1653330261,
$exception
);
}
}
src/Cache/FileSystemCache.php 0000644 00000006313 14350444705 0012070 0 ustar 00
*/
final class FileSystemCache implements CacheInterface
{
/** @var array> */
private array $delegates;
public function __construct(string $cacheDir = null)
{
$cacheDir ??= sys_get_temp_dir();
// @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()),
];
}
public function has($key): bool
{
foreach ($this->delegates as $delegate) {
if ($delegate->has($key)) {
return true;
}
}
return false;
}
public function get($key, $default = null): mixed
{
foreach ($this->delegates as $delegate) {
if ($delegate->has($key)) {
return $delegate->get($key, $default);
}
}
return $default;
}
public function set($key, $value, $ttl = null): bool
{
$delegate = $this->delegates['*'];
if (is_object($value) && isset($this->delegates[$value::class])) {
$delegate = $this->delegates[$value::class];
}
return $delegate->set($key, $value, $ttl);
}
public function delete($key): bool
{
$deleted = true;
foreach ($this->delegates as $delegate) {
$deleted = $delegate->delete($key) && $deleted;
}
return $deleted;
}
public function clear(): bool
{
$cleared = true;
foreach ($this->delegates as $delegate) {
$cleared = $delegate->clear() && $cleared;
}
return $cleared;
}
/**
* @return Traversable
*/
public function getMultiple($keys, $default = null): Traversable
{
foreach ($keys as $key) {
yield $key => $this->get($key, $default);
}
}
public function setMultiple($values, $ttl = null): bool
{
$set = true;
foreach ($values as $key => $value) {
$set = $this->set($key, $value, $ttl) && $set;
}
return $set;
}
public function deleteMultiple($keys): bool
{
$deleted = true;
foreach ($keys as $key) {
$deleted = $this->delete($key) && $deleted;
}
return $deleted;
}
}
src/Cache/FileWatchingCache.php 0000644 00000007501 14350444705 0012350 0 ustar 00
* @template EntryType
* @implements CacheInterface
*/
final class FileWatchingCache implements CacheInterface
{
/** @var array */
private array $timestamps = [];
public function __construct(
/** @var CacheInterface */
private CacheInterface $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): mixed
{
if (! $this->has($key)) {
return $default;
}
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
}
private function saveTimestamps(string $key, mixed $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]);
}
}
}
src/Cache/KeySanitizerCache.php 0000644 00000004745 14350444705 0012434 0 ustar 00
*/
final class KeySanitizerCache implements CacheInterface
{
private static string $version;
/** @var Closure(string): string */
private Closure $sanitize;
public function __construct(
/** @var CacheInterface */
private CacheInterface $delegate
) {
// Two things:
// 1. We append the current version of the package to the cache key in
// order to avoid collisions between entries from different versions
// of the library.
// 2. The key is sha1'd so that it does not contain illegal characters.
// @see https://www.php-fig.org/psr/psr-16/#12-definitions
// @infection-ignore-all
$this->sanitize = static fn (string $key) => sha1("$key." . self::$version ??= PHP_VERSION . '/' . Package::version());
}
public function get($key, $default = null): mixed
{
return $this->delegate->get(($this->sanitize)($key), $default);
}
public function set($key, $value, $ttl = null): bool
{
return $this->delegate->set(($this->sanitize)($key), $value, $ttl);
}
public function delete($key): bool
{
return $this->delegate->delete(($this->sanitize)($key));
}
public function clear(): bool
{
return $this->delegate->clear();
}
public function has($key): bool
{
return $this->delegate->has(($this->sanitize)($key));
}
/**
* @return Traversable
*/
public function getMultiple($keys, $default = null): Traversable
{
foreach ($keys as $key) {
yield $key => $this->delegate->get(($this->sanitize)($key), $default);
}
}
public function setMultiple($values, $ttl = null): bool
{
$versionedValues = [];
foreach ($values as $key => $value) {
$versionedValues[($this->sanitize)($key)] = $value;
}
return $this->delegate->setMultiple($versionedValues, $ttl);
}
public function deleteMultiple($keys): bool
{
$transformedKeys = [];
foreach ($keys as $key) {
$transformedKeys[] = ($this->sanitize)($key);
}
return $this->delegate->deleteMultiple($transformedKeys);
}
}
src/Cache/RuntimeCache.php 0000644 00000003170 14350444705 0011425 0 ustar 00
*/
final class RuntimeCache implements CacheInterface
{
/** @var array */
private array $entries = [];
public function get($key, $default = null): mixed
{
return $this->entries[$key] ?? $default;
}
public function set($key, $value, $ttl = null): bool
{
$this->entries[$key] = $value;
return true;
}
public function delete($key): bool
{
unset($this->entries[$key]);
return true;
}
public function clear(): bool
{
$this->entries = [];
return true;
}
public function getMultiple($keys, $default = null): iterable
{
$entries = [];
foreach ($keys as $key) {
$entries[$key] = $this->get($key, $default);
}
return $entries;
}
public function setMultiple($values, $ttl = null): bool
{
foreach ($values as $key => $value) {
$this->set($key, $value, $ttl);
}
return true;
}
public function deleteMultiple($keys): bool
{
foreach ($keys as $key) {
$this->delete($key);
}
return true;
}
public function has($key): bool
{
return isset($this->entries[$key]);
}
}
src/Cache/Warmup/RecursiveCacheWarmupService.php 0000644 00000005372 14350444705 0015747 0 ustar 00 */
private array $classesWarmedUp = [];
public function __construct(
private TypeParser $parser,
private ObjectImplementations $implementations,
private ClassDefinitionRepository $classDefinitionRepository,
private ObjectBuilderFactory $objectBuilderFactory
) {
}
public function warmup(string ...$signatures): void
{
foreach ($signatures as $signature) {
try {
$this->warmupType($this->parser->parse($signature));
} catch (InvalidType $exception) {
throw new InvalidSignatureToWarmup($signature, $exception);
}
}
}
private function warmupType(Type $type): void
{
if ($type instanceof InterfaceType) {
$this->warmupInterfaceType($type);
}
if ($type instanceof ClassType) {
$this->warmupClassType($type);
}
if ($type instanceof CompositeType) {
foreach ($type->traverse() as $subType) {
$this->warmupType($subType);
}
}
}
private function warmupInterfaceType(InterfaceType $type): void
{
$interfaceName = $type->className();
if (! $this->implementations->has($interfaceName)) {
return;
}
$function = $this->implementations->function($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());
}
}
}
}
src/Definition/Attributes.php 0000644 00000000773 14350444705 0012277 0 ustar 00
*/
interface Attributes extends IteratorAggregate, Countable
{
/**
* @param class-string $className
*/
public function has(string $className): bool;
/**
* @template T of object
*
* @param class-string $className
* @return list
*/
public function ofType(string $className): array;
}
src/Definition/AttributesContainer.php 0000644 00000002352 14350444705 0014135 0 ustar 00 */
private array $attributes;
public function __construct(object ...$attributes)
{
$this->attributes = $attributes;
}
public static function empty(): self
{
return self::$empty ??= new self();
}
public function has(string $className): bool
{
foreach ($this->attributes as $attribute) {
if ($attribute instanceof $className) {
return true;
}
}
return false;
}
public function ofType(string $className): array
{
return array_values(array_filter(
$this->attributes,
static fn (object $attribute): bool => $attribute instanceof $className
));
}
public function count(): int
{
return count($this->attributes);
}
/**
* @return Traversable
*/
public function getIterator(): Traversable
{
return yield from $this->attributes;
}
}
src/Definition/ClassDefinition.php 0000644 00000002046 14350444705 0013222 0 ustar 00 type->className();
}
public function type(): ClassType
{
return $this->type;
}
public function attributes(): Attributes
{
return $this->attributes;
}
public function properties(): Properties
{
return $this->properties;
}
public function methods(): Methods
{
return $this->methods;
}
public function isFinal(): bool
{
return $this->isFinal;
}
public function isAbstract(): bool
{
return $this->isAbstract;
}
}
src/Definition/Exception/ClassTypeAliasesDuplication.php 0000644 00000001031 14350444705 0017500 0 ustar 00 count() - 1;
parent::__construct(
"Index $index is out of range, it should be between 0 and $max.",
1644936619
);
}
}
src/Definition/Exception/InvalidTypeAliasImportClass.php 0000644 00000001017 14350444705 0017462 0 ustar 00 className()}`.",
1638535486
);
}
}
src/Definition/Exception/InvalidTypeAliasImportClassType.php 0000644 00000001027 14350444705 0020325 0 ustar 00 toString()}` was given in class `{$classType->className()}`.",
1638535608
);
}
}
src/Definition/Exception/MethodNotFound.php 0000644 00000000532 14350444705 0014775 0 ustar 00 toString()}` (docblock) does not accept `{$typeFromReflection->toString()}` (native).";
} elseif ($reflection instanceof ReflectionParameter) {
$message = "Types for parameter `$signature` do not match: `{$typeFromDocBlock->toString()}` (docblock) does not accept `{$typeFromReflection->toString()}` (native).";
} else {
$message = "Return types for method `$signature` do not match: `{$typeFromDocBlock->toString()}` (docblock) does not accept `{$typeFromReflection->toString()}` (native).";
}
parent::__construct($message, 1638471381);
}
}
src/Definition/Exception/UnknownTypeAliasImport.php 0000644 00000001051 14350444705 0016543 0 ustar 00 className()}` could not be found in `$importClassName`",
1638535757
);
}
}
src/Definition/FunctionDefinition.php 0000644 00000002514 14350444705 0013742 0 ustar 00 name;
}
public function signature(): string
{
return $this->signature;
}
public function attributes(): Attributes
{
return $this->attributes;
}
public function fileName(): ?string
{
return $this->fileName;
}
/**
* @return class-string|null
*/
public function class(): ?string
{
return $this->class;
}
public function isStatic(): bool
{
return $this->isStatic;
}
public function isClosure(): bool
{
return $this->isClosure;
}
public function parameters(): Parameters
{
return $this->parameters;
}
public function returnType(): Type
{
return $this->returnType;
}
}
src/Definition/FunctionObject.php 0000644 00000001073 14350444705 0013057 0 ustar 00 definition = $definition;
$this->callback = $callback;
}
public function definition(): FunctionDefinition
{
return $this->definition;
}
public function callback(): callable
{
return $this->callback;
}
}
src/Definition/FunctionsContainer.php 0000644 00000002370 14350444705 0013757 0 ustar 00
*/
final class FunctionsContainer implements IteratorAggregate
{
/** @var array */
private array $functions = [];
public function __construct(
private FunctionDefinitionRepository $functionDefinitionRepository,
/** @var array */
private array $callables
) {
}
public function has(string|int $key): bool
{
return isset($this->callables[$key]);
}
public function get(string|int $key): FunctionObject
{
return $this->function($key);
}
public function getIterator(): Traversable
{
foreach (array_keys($this->callables) as $key) {
yield $key => $this->function($key);
}
}
private function function(string|int $key): FunctionObject
{
return $this->functions[$key] ??= new FunctionObject(
$this->functionDefinitionRepository->for($this->callables[$key]),
$this->callables[$key]
);
}
}
src/Definition/MethodDefinition.php 0000644 00000001605 14350444705 0013375 0 ustar 00 name;
}
public function signature(): string
{
return $this->signature;
}
public function parameters(): Parameters
{
return $this->parameters;
}
public function isStatic(): bool
{
return $this->isStatic;
}
public function isPublic(): bool
{
return $this->isPublic;
}
public function returnType(): Type
{
return $this->returnType;
}
}
src/Definition/Methods.php 0000644 00000002457 14350444705 0011555 0 ustar 00
*/
final class Methods implements IteratorAggregate, Countable
{
/** @var MethodDefinition[] */
private array $methods = [];
public function __construct(MethodDefinition ...$methods)
{
foreach ($methods as $method) {
$this->methods[$method->name()] = $method;
}
}
public function has(string $name): bool
{
return isset($this->methods[$name]);
}
public function get(string $name): MethodDefinition
{
if (! $this->has($name)) {
throw new MethodNotFound($name);
}
return $this->methods[$name];
}
public function hasConstructor(): bool
{
return $this->has('__construct');
}
public function constructor(): MethodDefinition
{
return $this->get('__construct');
}
public function count(): int
{
return count($this->methods);
}
/**
* @return Traversable
*/
public function getIterator(): Traversable
{
yield from $this->methods;
}
}
src/Definition/NativeAttributes.php 0000644 00000004633 14350444705 0013445 0 ustar 00 > */
private array $reflectionAttributes;
/**
* @param ReflectionClass|ReflectionProperty|ReflectionMethod|ReflectionFunction|ReflectionParameter $reflection
*/
public function __construct(ReflectionClass|ReflectionProperty|ReflectionMethod|ReflectionFunction|ReflectionParameter $reflection)
{
$this->reflectionAttributes = $reflection->getAttributes();
$attributes = array_filter(
array_map(
static function (ReflectionAttribute $attribute) {
try {
return $attribute->newInstance();
} catch (Error) {
// Race condition when the attribute is affected to a property/parameter
// that was PROMOTED, in this case the attribute will be applied to both
// ParameterReflection AND PropertyReflection, BUT the target arg inside the attribute
// class is configured to support only ONE of them (parameter OR property)
// https://wiki.php.net/rfc/constructor_promotion#attributes for more details.
// Ignore attribute if the instantiation failed.
return null;
}
},
$this->reflectionAttributes,
),
);
$this->delegate = new AttributesContainer(...$attributes);
}
public function has(string $className): bool
{
return $this->delegate->has($className);
}
public function ofType(string $className): array
{
return $this->delegate->ofType($className);
}
public function getIterator(): Traversable
{
yield from $this->delegate;
}
public function count(): int
{
return count($this->delegate);
}
/**
* @return array>
*/
public function reflectionAttributes(): array
{
return $this->reflectionAttributes;
}
}
src/Definition/ParameterDefinition.php 0000644 00000002002 14350444705 0014065 0 ustar 00 name;
}
public function signature(): string
{
return $this->signature;
}
public function type(): Type
{
return $this->type;
}
public function isOptional(): bool
{
return $this->isOptional;
}
public function isVariadic(): bool
{
return $this->isVariadic;
}
public function defaultValue(): mixed
{
return $this->defaultValue;
}
public function attributes(): Attributes
{
return $this->attributes;
}
}
src/Definition/Parameters.php 0000644 00000003021 14350444705 0012241 0 ustar 00
*/
final class Parameters implements IteratorAggregate, Countable
{
/** @var ParameterDefinition[] */
private array $parameters = [];
public function __construct(ParameterDefinition ...$parameters)
{
foreach ($parameters as $parameter) {
$this->parameters[$parameter->name()] = $parameter;
}
}
public function has(string $name): bool
{
return isset($this->parameters[$name]);
}
public function get(string $name): ParameterDefinition
{
if (! $this->has($name)) {
throw new ParameterNotFound($name);
}
return $this->parameters[$name];
}
/**
* @param int<0, max> $index
*/
public function at(int $index): ParameterDefinition
{
if ($index >= $this->count()) {
throw new InvalidParameterIndex($index, $this);
}
return array_values($this->parameters)[$index];
}
public function count(): int
{
return count($this->parameters);
}
/**
* @return Traversable
*/
public function getIterator(): Traversable
{
yield from $this->parameters;
}
}
src/Definition/Properties.php 0000644 00000002222 14350444705 0012274 0 ustar 00
*/
final class Properties implements IteratorAggregate, Countable
{
/** @var PropertyDefinition[] */
private array $properties = [];
public function __construct(PropertyDefinition ...$properties)
{
foreach ($properties as $property) {
$this->properties[$property->name()] = $property;
}
}
public function has(string $name): bool
{
return isset($this->properties[$name]);
}
public function get(string $name): PropertyDefinition
{
if (! $this->has($name)) {
throw new PropertyNotFound($name);
}
return $this->properties[$name];
}
public function count(): int
{
return count($this->properties);
}
/**
* @return Traversable
*/
public function getIterator(): Traversable
{
yield from $this->properties;
}
}
src/Definition/PropertyDefinition.php 0000644 00000002012 14350444705 0013772 0 ustar 00 name;
}
public function signature(): string
{
return $this->signature;
}
public function type(): Type
{
return $this->type;
}
public function hasDefaultValue(): bool
{
return $this->hasDefaultValue;
}
public function defaultValue(): mixed
{
return $this->defaultValue;
}
public function isPublic(): bool
{
return $this->isPublic;
}
public function attributes(): Attributes
{
return $this->attributes;
}
}
src/Definition/Repository/AttributesRepository.php 0000644 00000001071 14350444705 0016546 0 ustar 00 |ReflectionProperty|ReflectionMethod|ReflectionFunction|ReflectionParameter $reflector
*/
public function for(ReflectionClass|ReflectionProperty|ReflectionMethod|ReflectionFunction|ReflectionParameter $reflector): Attributes;
}
src/Definition/Repository/Cache/CacheClassDefinitionRepository.php 0000644 00000001612 14350444705 0021426 0 ustar 00 */
private CacheInterface $cache
) {
}
public function for(ClassType $type): ClassDefinition
{
$key = "class-definition-{$type->toString()}";
$entry = $this->cache->get($key);
if ($entry) {
return $entry;
}
$class = $this->delegate->for($type);
$this->cache->set($key, $class);
return $class;
}
}
src/Definition/Repository/Cache/CacheFunctionDefinitionRepository.php 0000644 00000002073 14350444705 0022150 0 ustar 00 */
private CacheInterface $cache
) {
}
public function for(callable $function): FunctionDefinition
{
$reflection = Reflection::function($function);
$key = "function-definition-{$reflection->getFileName()}-{$reflection->getStartLine()}-{$reflection->getEndLine()}";
$entry = $this->cache->get($key);
if ($entry) {
return $entry;
}
$definition = $this->delegate->for($function);
$this->cache->set($key, $definition);
return $definition;
}
}
src/Definition/Repository/Cache/Compiler/AttributesCompiler.php 0000644 00000003365 14350444705 0020726 0 ustar 00 compileNativeAttributes($attributes);
return << $reflectionAttribute
*/
private function compileNativeAttribute(ReflectionAttribute $reflectionAttribute): string
{
$name = $reflectionAttribute->getName();
$arguments = $reflectionAttribute->getArguments();
/** @infection-ignore-all */
if (count($arguments) > 0) {
$arguments = serialize($arguments);
$arguments = 'unserialize(' . var_export($arguments, true) . ')';
return "new $name(...$arguments)";
}
return "new $name()";
}
private function compileNativeAttributes(NativeAttributes $attributes): string
{
$attributesListCode = array_map(
fn (ReflectionAttribute $attribute) => $this->compileNativeAttribute($attribute),
$attributes->reflectionAttributes()
);
return '...[' . implode(",\n", $attributesListCode) . ']';
}
}
src/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php 0000644 00000004244 14350444705 0021653 0 ustar 00 typeCompiler = new TypeCompiler();
$this->attributesCompiler = new AttributesCompiler();
$this->methodCompiler = new MethodDefinitionCompiler($this->typeCompiler, $this->attributesCompiler);
$this->propertyCompiler = new PropertyDefinitionCompiler($this->typeCompiler, $this->attributesCompiler);
}
public function compile(mixed $value): string
{
assert($value instanceof ClassDefinition);
$type = $this->typeCompiler->compile($value->type());
$properties = array_map(
fn (PropertyDefinition $property) => $this->propertyCompiler->compile($property),
iterator_to_array($value->properties())
);
$properties = implode(', ', $properties);
$methods = array_map(
fn (MethodDefinition $method) => $this->methodCompiler->compile($method),
iterator_to_array($value->methods())
);
$methods = implode(', ', $methods);
$attributes = $this->attributesCompiler->compile($value->attributes());
$isFinal = var_export($value->isFinal(), true);
$isAbstract = var_export($value->isAbstract(), true);
return <<