update lock clucknut
All checks were successful
All checks were successful
This commit is contained in:
6
vendor/cuyz/valinor/README.md
vendored
6
vendor/cuyz/valinor/README.md
vendored
@@ -36,7 +36,7 @@ ## Installation
|
||||
composer require cuyz/valinor
|
||||
```
|
||||
|
||||
**📔 Read more on the [online documentation](https://valinor.cuyz.io)**
|
||||
**📔 Read more on the [online documentation](https://valinor-php.dev)**
|
||||
|
||||
## Example
|
||||
|
||||
@@ -86,7 +86,7 @@ ## Example
|
||||
|
||||
## Documentation
|
||||
|
||||
The full documentation is available on [valinor.cuyz.io].
|
||||
The full documentation is available on [valinor-php.dev].
|
||||
|
||||
## Credits & thank you
|
||||
|
||||
@@ -126,4 +126,4 @@ ### Special thanks
|
||||
|
||||
[blackfire-logo]: docs/pages/img/blackfire-logo.svg "Blackfire logo"
|
||||
|
||||
[valinor.cuyz.io]: https://valinor.cuyz.io
|
||||
[valinor-php.dev]: https://valinor-php.dev
|
||||
|
||||
22
vendor/cuyz/valinor/composer.json
vendored
22
vendor/cuyz/valinor/composer.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cuyz/valinor",
|
||||
"type": "library",
|
||||
"description": "Library that helps to map any input into a strongly-typed value object structure.",
|
||||
"description": "Dependency free PHP library that helps to map any input into a strongly-typed structure.",
|
||||
"keywords": [
|
||||
"object", "tree", "mapper", "mapping", "hydrator", "array", "conversion", "json", "yaml"
|
||||
],
|
||||
@@ -15,21 +15,21 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"composer-runtime-api": "^2.0"
|
||||
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"infection/infection": "^0.31",
|
||||
"phpunit/phpunit": "^11.5",
|
||||
"infection/infection": "^0.32",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.4",
|
||||
"friendsofphp/php-cs-fixer": "^3.91",
|
||||
"marcocesarato/php-conventional-changelog": "^1.12",
|
||||
"vimeo/psalm": "^6.0",
|
||||
"mikey179/vfsstream": "^1.6.10",
|
||||
"rector/rector": "^2.0",
|
||||
"phpbench/phpbench": "^1.3"
|
||||
"phpbench/phpbench": "^1.3",
|
||||
"psr/http-message": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -47,12 +47,12 @@
|
||||
"@putenv XDEBUG_MODE=off",
|
||||
"phpunit",
|
||||
"phpstan",
|
||||
"php-cs-fixer check --diff",
|
||||
"rector --dry-run",
|
||||
"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",
|
||||
"php-cs-fixer fix --dry-run",
|
||||
"rector --dry-run",
|
||||
"@check-todo"
|
||||
],
|
||||
"check-todo": [
|
||||
@@ -60,7 +60,7 @@
|
||||
],
|
||||
"fix": [
|
||||
"@putenv XDEBUG_MODE=off",
|
||||
"php-cs-fixer fix",
|
||||
"php-cs-fixer fix --diff",
|
||||
"rector"
|
||||
],
|
||||
"test": [
|
||||
@@ -68,7 +68,7 @@
|
||||
"phpunit"
|
||||
],
|
||||
"mutation": [
|
||||
"infection --threads=max --git-diff-lines --with-uncovered --min-msi=100"
|
||||
"infection --threads=max --git-diff-lines --min-msi=100 --with-timeouts --with-uncovered --show-mutations"
|
||||
],
|
||||
"doc": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
use PHPStan\Rules\Rule;
|
||||
use PHPStan\Rules\RuleErrorBuilder;
|
||||
|
||||
use function preg_match;
|
||||
use function str_contains;
|
||||
use function str_starts_with;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
use PHPStan\Type\Type;
|
||||
use PHPStan\Type\UnionType;
|
||||
|
||||
use function count;
|
||||
use function implode;
|
||||
use function method_exists;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function count;
|
||||
|
||||
final class TreeMapperPsalmPlugin implements MethodReturnTypeProviderInterface
|
||||
{
|
||||
public static function getClassLikeNames(): array
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<InvalidArgument>
|
||||
<errorLevel type="suppress">
|
||||
<referencedFunction name="CuyZ\Valinor\NormalizerBuilder::registerTransformer"/>
|
||||
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::infer"/>
|
||||
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::registerConstructor"/>
|
||||
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::registerConverter"/>
|
||||
<referencedFunction name="CuyZ\Valinor\MapperBuilder::infer"/>
|
||||
<referencedFunction name="CuyZ\Valinor\MapperBuilder::registerConstructor"/>
|
||||
<referencedFunction name="CuyZ\Valinor\MapperBuilder::registerConverter"/>
|
||||
</errorLevel>
|
||||
</InvalidArgument>
|
||||
</issueHandlers>
|
||||
|
||||
8
vendor/cuyz/valinor/src/Cache/CacheEntry.php
vendored
8
vendor/cuyz/valinor/src/Cache/CacheEntry.php
vendored
@@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
177
vendor/cuyz/valinor/src/Cache/TypeFilesWatcher.php
vendored
177
vendor/cuyz/valinor/src/Cache/TypeFilesWatcher.php
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
namespace CuyZ\Valinor\Compiler;
|
||||
|
||||
use function array_shift;
|
||||
use function str_repeat;
|
||||
use function str_replace;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use CuyZ\Valinor\Compiler\Compiler;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
|
||||
use function implode;
|
||||
use function var_export;
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
use CuyZ\Valinor\Compiler\Compiler;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
|
||||
use function array_shift;
|
||||
|
||||
/** @internal */
|
||||
final class LogicalAndNode extends Node
|
||||
{
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
use CuyZ\Valinor\Compiler\Compiler;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
|
||||
use function array_shift;
|
||||
|
||||
/** @internal */
|
||||
final class LogicalOrNode extends Node
|
||||
{
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
use CuyZ\Valinor\Compiler\Compiler;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
|
||||
use function implode;
|
||||
|
||||
/** @internal */
|
||||
final class MatchNode extends Node
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
|
||||
use function array_map;
|
||||
use function implode;
|
||||
|
||||
/** @internal */
|
||||
final class MethodNode extends Node
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
|
||||
use function array_map;
|
||||
use function implode;
|
||||
|
||||
/** @internal */
|
||||
final class NewClassNode extends Node
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
use CuyZ\Valinor\Compiler\Compiler;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
|
||||
use function array_map;
|
||||
use function implode;
|
||||
|
||||
/** @internal */
|
||||
final class ShortClosureNode extends Node
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use CuyZ\Valinor\Compiler\Compiler;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function var_export;
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function is_a;
|
||||
use function reset;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -63,10 +65,26 @@ public function has(string $className): bool
|
||||
public function filter(callable $callback): self
|
||||
{
|
||||
return new self(
|
||||
...array_filter($this->attributes, $callback)
|
||||
...array_filter($this->attributes, $callback),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $className
|
||||
*/
|
||||
public function firstOf(string $className): AttributeDefinition
|
||||
{
|
||||
assert($this->has($className));
|
||||
|
||||
$filtered = array_filter(
|
||||
$this->attributes,
|
||||
static fn (AttributeDefinition $attribute) => is_a($attribute->class->type->className(), $className, true),
|
||||
);
|
||||
|
||||
/** @var AttributeDefinition */
|
||||
return reset($filtered);
|
||||
}
|
||||
|
||||
public function merge(self $other): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
|
||||
/** @internal */
|
||||
final class ClassDefinition
|
||||
final readonly class ClassDefinition
|
||||
{
|
||||
public function __construct(
|
||||
/** @var class-string */
|
||||
public readonly string $name,
|
||||
public readonly ObjectType $type,
|
||||
public readonly Attributes $attributes,
|
||||
public readonly Properties $properties,
|
||||
public readonly Methods $methods,
|
||||
public readonly bool $isFinal,
|
||||
public readonly bool $isAbstract,
|
||||
public string $name,
|
||||
public ObjectType $type,
|
||||
public Attributes $attributes,
|
||||
public Properties $properties,
|
||||
public Methods $methods,
|
||||
public bool $isFinal,
|
||||
public bool $isAbstract,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -9,22 +9,22 @@
|
||||
use CuyZ\Valinor\Utility\TypeHelper;
|
||||
|
||||
/** @internal */
|
||||
final class FunctionDefinition
|
||||
final readonly class FunctionDefinition
|
||||
{
|
||||
public function __construct(
|
||||
/** @var non-empty-string */
|
||||
public readonly string $name,
|
||||
public string $name,
|
||||
/** @var non-empty-string */
|
||||
public readonly string $signature,
|
||||
public readonly Attributes $attributes,
|
||||
public string $signature,
|
||||
public Attributes $attributes,
|
||||
/** @var non-empty-string|null */
|
||||
public readonly ?string $fileName,
|
||||
public ?string $fileName,
|
||||
/** @var class-string|null */
|
||||
public readonly ?string $class,
|
||||
public readonly bool $isStatic,
|
||||
public readonly bool $isClosure,
|
||||
public readonly Parameters $parameters,
|
||||
public readonly Type $returnType,
|
||||
public ?string $class,
|
||||
public bool $isStatic,
|
||||
public bool $isClosure,
|
||||
public Parameters $parameters,
|
||||
public Type $returnType,
|
||||
) {}
|
||||
|
||||
public function forCallable(callable $callable): self
|
||||
|
||||
@@ -5,16 +5,11 @@
|
||||
namespace CuyZ\Valinor\Definition;
|
||||
|
||||
/** @internal */
|
||||
final class FunctionObject
|
||||
final readonly class FunctionObject
|
||||
{
|
||||
public readonly FunctionDefinition $definition;
|
||||
|
||||
/** @var callable */
|
||||
public readonly mixed $callback;
|
||||
|
||||
public function __construct(FunctionDefinition $definition, callable $callback)
|
||||
{
|
||||
$this->definition = $definition;
|
||||
$this->callback = $callback;
|
||||
}
|
||||
public function __construct(
|
||||
public FunctionDefinition $definition,
|
||||
/** @var callable */
|
||||
public mixed $callback,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
|
||||
/** @internal */
|
||||
final class MethodDefinition
|
||||
final readonly class MethodDefinition
|
||||
{
|
||||
public function __construct(
|
||||
/** @var non-empty-string */
|
||||
public readonly string $name,
|
||||
public string $name,
|
||||
/** @var non-empty-string */
|
||||
public readonly string $signature,
|
||||
public readonly Attributes $attributes,
|
||||
public readonly Parameters $parameters,
|
||||
public readonly bool $isStatic,
|
||||
public readonly bool $isPublic,
|
||||
public readonly Type $returnType
|
||||
public string $signature,
|
||||
public Attributes $attributes,
|
||||
public Parameters $parameters,
|
||||
public bool $isStatic,
|
||||
public bool $isPublic,
|
||||
public Type $returnType
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
|
||||
@@ -8,20 +8,22 @@
|
||||
use CuyZ\Valinor\Type\Types\Generics;
|
||||
use CuyZ\Valinor\Utility\TypeHelper;
|
||||
|
||||
use function assert;
|
||||
|
||||
/** @internal */
|
||||
final class ParameterDefinition
|
||||
final readonly class ParameterDefinition
|
||||
{
|
||||
public function __construct(
|
||||
/** @var non-empty-string */
|
||||
public readonly string $name,
|
||||
public string $name,
|
||||
/** @var non-empty-string */
|
||||
public readonly string $signature,
|
||||
public readonly Type $type,
|
||||
public readonly Type $nativeType,
|
||||
public readonly bool $isOptional,
|
||||
public readonly bool $isVariadic,
|
||||
public readonly mixed $defaultValue,
|
||||
public readonly Attributes $attributes
|
||||
public string $signature,
|
||||
public Type $type,
|
||||
public Type $nativeType,
|
||||
public bool $isOptional,
|
||||
public bool $isVariadic,
|
||||
public mixed $defaultValue,
|
||||
public Attributes $attributes
|
||||
) {}
|
||||
|
||||
public function forCallable(callable $callable): self
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
|
||||
/** @internal */
|
||||
final class PropertyDefinition
|
||||
final readonly class PropertyDefinition
|
||||
{
|
||||
public function __construct(
|
||||
/** @var non-empty-string */
|
||||
public readonly string $name,
|
||||
public string $name,
|
||||
/** @var non-empty-string */
|
||||
public readonly string $signature,
|
||||
public readonly Type $type,
|
||||
public readonly Type $nativeType,
|
||||
public readonly bool $hasDefaultValue,
|
||||
public readonly mixed $defaultValue,
|
||||
public readonly bool $isPublic,
|
||||
public readonly Attributes $attributes
|
||||
public string $signature,
|
||||
public Type $type,
|
||||
public Type $nativeType,
|
||||
public bool $hasDefaultValue,
|
||||
public mixed $defaultValue,
|
||||
public bool $isPublic,
|
||||
public Attributes $attributes
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
|
||||
use CuyZ\Valinor\Cache\Cache;
|
||||
use CuyZ\Valinor\Cache\CacheEntry;
|
||||
use CuyZ\Valinor\Cache\TypeFilesWatcher;
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Definition\Repository\Cache\Compiler\ClassDefinitionCompiler;
|
||||
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
use CuyZ\Valinor\Utility\Reflection\Reflection;
|
||||
|
||||
use function is_string;
|
||||
|
||||
/** @internal */
|
||||
final class CompiledClassDefinitionRepository implements ClassDefinitionRepository
|
||||
@@ -21,6 +19,7 @@ public function __construct(
|
||||
private ClassDefinitionRepository $delegate,
|
||||
/** @var Cache<ClassDefinition> */
|
||||
private Cache $cache,
|
||||
private TypeFilesWatcher $filesWatcher,
|
||||
private ClassDefinitionCompiler $compiler,
|
||||
) {}
|
||||
|
||||
@@ -38,31 +37,11 @@ public function for(ObjectType $type): ClassDefinition
|
||||
$class = $this->delegate->for($type);
|
||||
|
||||
$code = 'fn () => ' . $this->compiler->compile($class);
|
||||
$filesToWatch = $this->filesToWatch($type);
|
||||
$filesToWatch = fn () => $this->filesWatcher->for($type);
|
||||
|
||||
$this->cache->set($key, new CacheEntry($code, $filesToWatch));
|
||||
|
||||
/** @var ClassDefinition */
|
||||
return $this->cache->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<non-empty-string>
|
||||
*/
|
||||
private function filesToWatch(ObjectType $type): array
|
||||
{
|
||||
$reflection = Reflection::class($type->className());
|
||||
|
||||
$fileNames = [];
|
||||
|
||||
do {
|
||||
$fileName = $reflection->getFileName();
|
||||
|
||||
if (is_string($fileName)) {
|
||||
$fileNames[] = $fileName;
|
||||
}
|
||||
} while ($reflection = $reflection->getParentClass());
|
||||
|
||||
return $fileNames;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
use CuyZ\Valinor\Cache\Cache;
|
||||
use CuyZ\Valinor\Cache\CacheEntry;
|
||||
use CuyZ\Valinor\Cache\TypeFilesWatcher;
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\Repository\Cache\Compiler\FunctionDefinitionCompiler;
|
||||
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
|
||||
@@ -18,6 +19,7 @@ public function __construct(
|
||||
private FunctionDefinitionRepository $delegate,
|
||||
/** @var Cache<FunctionDefinition> */
|
||||
private Cache $cache,
|
||||
private TypeFilesWatcher $filesWatcher,
|
||||
private FunctionDefinitionCompiler $compiler,
|
||||
) {}
|
||||
|
||||
@@ -37,7 +39,7 @@ public function for(callable $function): FunctionDefinition
|
||||
$definition = $this->delegate->for($function);
|
||||
|
||||
$code = 'fn () => ' . $this->compiler->compile($definition);
|
||||
$filesToWatch = $definition->fileName ? [$definition->fileName] : [];
|
||||
$filesToWatch = fn () => $this->filesWatcher->for($function);
|
||||
|
||||
$this->cache->set($key, new CacheEntry($code, $filesToWatch));
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use CuyZ\Valinor\Definition\PropertyDefinition;
|
||||
|
||||
use function array_map;
|
||||
use function implode;
|
||||
@@ -39,14 +37,14 @@ public function compile(ClassDefinition $value): string
|
||||
$type = $this->typeCompiler->compile($value->type);
|
||||
|
||||
$properties = array_map(
|
||||
fn (PropertyDefinition $property) => $this->propertyCompiler->compile($property),
|
||||
$this->propertyCompiler->compile(...),
|
||||
iterator_to_array($value->properties)
|
||||
);
|
||||
|
||||
$properties = implode(', ', $properties);
|
||||
|
||||
$methods = array_map(
|
||||
fn (MethodDefinition $method) => $this->methodCompiler->compile($method),
|
||||
$this->methodCompiler->compile(...),
|
||||
iterator_to_array($value->methods)
|
||||
);
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\ParameterDefinition;
|
||||
|
||||
use function array_map;
|
||||
use function implode;
|
||||
use function iterator_to_array;
|
||||
use function var_export;
|
||||
|
||||
/** @internal */
|
||||
@@ -29,7 +31,7 @@ public function __construct()
|
||||
public function compile(FunctionDefinition $value): string
|
||||
{
|
||||
$parameters = array_map(
|
||||
fn (ParameterDefinition $parameter) => $this->parameterCompiler->compile($parameter),
|
||||
$this->parameterCompiler->compile(...),
|
||||
iterator_to_array($value->parameters)
|
||||
);
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
|
||||
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use CuyZ\Valinor\Definition\ParameterDefinition;
|
||||
|
||||
use function array_map;
|
||||
use function implode;
|
||||
use function iterator_to_array;
|
||||
use function var_export;
|
||||
|
||||
/** @internal */
|
||||
@@ -30,7 +32,7 @@ public function compile(MethodDefinition $method): string
|
||||
$attributes = $this->attributesCompiler->compile($method->attributes);
|
||||
|
||||
$parameters = array_map(
|
||||
fn (ParameterDefinition $parameter) => $this->parameterCompiler->compile($parameter),
|
||||
$this->parameterCompiler->compile(...),
|
||||
iterator_to_array($method->parameters)
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
use CuyZ\Valinor\Definition\ParameterDefinition;
|
||||
|
||||
use function is_object;
|
||||
use function serialize;
|
||||
use function var_export;
|
||||
|
||||
/** @internal */
|
||||
final class ParameterDefinitionCompiler
|
||||
{
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
use CuyZ\Valinor\Definition\PropertyDefinition;
|
||||
|
||||
use function var_export;
|
||||
|
||||
/** @internal */
|
||||
final class PropertyDefinitionCompiler
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function implode;
|
||||
use function str_ends_with;
|
||||
use function var_export;
|
||||
|
||||
/** @internal */
|
||||
@@ -94,7 +95,7 @@ public function compile(Type $type): string
|
||||
case $type instanceof IntersectionType:
|
||||
case $type instanceof UnionType:
|
||||
$subTypes = array_map(
|
||||
fn (Type $subType) => $this->compile($subType),
|
||||
$this->compile(...),
|
||||
$type->types()
|
||||
);
|
||||
|
||||
@@ -106,7 +107,7 @@ public function compile(Type $type): string
|
||||
'int' => "$class::integer()",
|
||||
default => (function () use ($type, $class) {
|
||||
$types = array_map(
|
||||
fn (Type $subType) => $this->compile($subType),
|
||||
$this->compile(...),
|
||||
$type->types,
|
||||
);
|
||||
|
||||
@@ -172,7 +173,7 @@ public function compile(Type $type): string
|
||||
return "new $class('{$type->className()}', [$generics])";
|
||||
case $type instanceof ClassStringType:
|
||||
$subTypes = implode(', ', array_map(
|
||||
fn (Type $subType) => $this->compile($subType),
|
||||
$this->compile(...),
|
||||
$type->subTypes(),
|
||||
));
|
||||
|
||||
@@ -192,7 +193,7 @@ public function compile(Type $type): string
|
||||
case $type instanceof CallableType:
|
||||
$returnType = $this->compile($type->returnType);
|
||||
$parameters = implode(', ', array_map(
|
||||
fn (Type $subType) => $this->compile($subType),
|
||||
$this->compile(...),
|
||||
$type->parameters,
|
||||
));
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public function for(Reflector $reflection): array
|
||||
$attributes = [];
|
||||
|
||||
foreach ($reflection->getAttributes() as $key => $attribute) {
|
||||
if (! $this->attributeIsAllowed($attribute) || ! $this->attributeCanBeInstantiated($attribute)) {
|
||||
if (! $this->attributeCanBeInstantiated($attribute) || ! $this->attributeIsAllowed($attribute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
use CuyZ\Valinor\Definition\PropertyDefinition;
|
||||
use CuyZ\Valinor\Definition\Repository\AttributesRepository;
|
||||
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
|
||||
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassGenericResolver;
|
||||
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassImportedTypeAliasResolver;
|
||||
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassLocalTypeAliasResolver;
|
||||
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassParentTypeResolver;
|
||||
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassGenericResolver;
|
||||
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ReflectionTypeResolver;
|
||||
use CuyZ\Valinor\Mapper\Object\Constructor;
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
@@ -30,6 +30,7 @@
|
||||
use ReflectionMethod;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function array_count_values;
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
use ReflectionParameter;
|
||||
|
||||
use function array_map;
|
||||
use function str_ends_with;
|
||||
use function str_starts_with;
|
||||
|
||||
/** @internal */
|
||||
final class ReflectionFunctionDefinitionRepository implements FunctionDefinitionRepository
|
||||
@@ -68,8 +66,6 @@ public function for(callable $function): FunctionDefinition
|
||||
$class = $reflection->getClosureScopeClass();
|
||||
$returnType = $returnTypeResolver->resolveReturnTypeFor($reflection);
|
||||
$nativeReturnType = $returnTypeResolver->resolveNativeReturnTypeFor($reflection);
|
||||
// PHP8.2 use `ReflectionFunction::isAnonymous()`
|
||||
$isClosure = $name === '{closure}' || str_ends_with($name, '\\{closure}') || str_starts_with($name, '{closure:');
|
||||
|
||||
if ($returnType instanceof UnresolvableType) {
|
||||
$returnType = $returnType->forFunctionReturnType($signature);
|
||||
@@ -84,7 +80,7 @@ public function for(callable $function): FunctionDefinition
|
||||
$reflection->getFileName() ?: null,
|
||||
$class?->name,
|
||||
$reflection->getClosureThis() === null,
|
||||
$isClosure,
|
||||
$reflection->isAnonymous(),
|
||||
new Parameters(...$parameters),
|
||||
$returnType,
|
||||
))->forCallable($function);
|
||||
@@ -95,8 +91,7 @@ public function for(callable $function): FunctionDefinition
|
||||
*/
|
||||
private function signature(ReflectionFunction $reflection): string
|
||||
{
|
||||
// PHP8.2 use `ReflectionFunction::isAnonymous()`
|
||||
if ($reflection->name === '{closure}' || str_ends_with($reflection->name, '\\{closure}') || str_starts_with($reflection->name, '{closure:')) {
|
||||
if ($reflection->isAnonymous()) {
|
||||
$startLine = $reflection->getStartLine();
|
||||
$endLine = $reflection->getEndLine();
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
use CuyZ\Valinor\Type\Types\UnresolvableType;
|
||||
use CuyZ\Valinor\Utility\Reflection\Reflection;
|
||||
|
||||
use function array_shift;
|
||||
|
||||
/** @internal */
|
||||
final class ClassGenericResolver
|
||||
{
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
namespace CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver;
|
||||
|
||||
use CuyZ\Valinor\Type\ObjectWithGenericType;
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
use CuyZ\Valinor\Type\ObjectWithGenericType;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
|
||||
use CuyZ\Valinor\Type\Parser\VacantTypeAssignerParser;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
|
||||
/** @internal */
|
||||
final class ClassParentTypeResolver
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
use CuyZ\Valinor\Utility\Reflection\Annotations;
|
||||
use ReflectionParameter;
|
||||
|
||||
use function array_search;
|
||||
|
||||
/** @internal */
|
||||
final class ParameterTypeResolver
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
use ReflectionType;
|
||||
use ReflectionUnionType;
|
||||
|
||||
use function implode;
|
||||
use function trim;
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
use ReflectionClass;
|
||||
use ReflectionFunctionAbstract;
|
||||
|
||||
use function array_key_exists;
|
||||
use function current;
|
||||
use function key;
|
||||
use function next;
|
||||
|
||||
/** @internal */
|
||||
final class TemplateResolver
|
||||
{
|
||||
|
||||
45
vendor/cuyz/valinor/src/Library/Container.php
vendored
45
vendor/cuyz/valinor/src/Library/Container.php
vendored
@@ -33,12 +33,15 @@
|
||||
use CuyZ\Valinor\Mapper\Object\Factory\StrictTypesObjectBuilderFactory;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ArrayNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ConverterContainer;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\HttpRequestNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\InterfaceInferringContainer;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\InterfaceNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\KeyConversionPipeline;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\KeyConverterNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ListNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\MixedNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\NodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\NullNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\InterfaceInferringContainer;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ObjectNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ScalarNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ShapedArrayNodeBuilder;
|
||||
@@ -118,6 +121,18 @@ public function __construct(Settings $settings)
|
||||
),
|
||||
);
|
||||
|
||||
if ($settings->keyConverters !== []) {
|
||||
$builder = new KeyConverterNodeBuilder(
|
||||
$builder,
|
||||
$this->get(KeyConversionPipeline::class),
|
||||
);
|
||||
}
|
||||
|
||||
$builder = new HttpRequestNodeBuilder(
|
||||
$builder,
|
||||
$this->get(KeyConversionPipeline::class),
|
||||
);
|
||||
|
||||
return new ValueConverterNodeBuilder(
|
||||
$builder,
|
||||
$this->get(ConverterContainer::class),
|
||||
@@ -132,6 +147,12 @@ public function __construct(Settings $settings)
|
||||
$settings->convertersSortedByPriority(),
|
||||
),
|
||||
|
||||
KeyConversionPipeline::class => fn () => new KeyConversionPipeline(
|
||||
$this->get(FunctionDefinitionRepository::class),
|
||||
$settings->keyConverters,
|
||||
$settings->exceptionFilter,
|
||||
),
|
||||
|
||||
InterfaceInferringContainer::class => fn () => new InterfaceInferringContainer(
|
||||
new FunctionsContainer(
|
||||
$this->get(FunctionDefinitionRepository::class),
|
||||
@@ -206,6 +227,7 @@ public function __construct(Settings $settings)
|
||||
$repository = new CompiledClassDefinitionRepository(
|
||||
$repository,
|
||||
$this->get(Cache::class),
|
||||
$this->get(TypeFilesWatcher::class),
|
||||
new ClassDefinitionCompiler(),
|
||||
);
|
||||
}
|
||||
@@ -226,6 +248,7 @@ public function __construct(Settings $settings)
|
||||
$repository = new CompiledFunctionDefinitionRepository(
|
||||
$repository,
|
||||
$this->get(Cache::class),
|
||||
$this->get(TypeFilesWatcher::class),
|
||||
new FunctionDefinitionCompiler(),
|
||||
);
|
||||
}
|
||||
@@ -256,10 +279,22 @@ public function __construct(Settings $settings)
|
||||
|
||||
Cache::class => fn () => new KeySanitizerCache($settings->cache, $settings),
|
||||
|
||||
TypeFilesWatcher::class => fn () => new TypeFilesWatcher(
|
||||
$settings,
|
||||
$this->get(ClassDefinitionRepository::class),
|
||||
),
|
||||
TypeFilesWatcher::class => function () use ($settings) {
|
||||
$classDefinitionRepository = new ReflectionClassDefinitionRepository(
|
||||
$this->get(TypeParserFactory::class),
|
||||
$settings->allowedAttributes(),
|
||||
);
|
||||
|
||||
$functionDefinitionRepository = new ReflectionFunctionDefinitionRepository(
|
||||
$this->get(TypeParserFactory::class),
|
||||
new ReflectionAttributesRepository(
|
||||
$classDefinitionRepository,
|
||||
$settings->allowedAttributes(),
|
||||
),
|
||||
);
|
||||
|
||||
return new TypeFilesWatcher($settings, $classDefinitionRepository, $functionDefinitionRepository);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
15
vendor/cuyz/valinor/src/Library/Settings.php
vendored
15
vendor/cuyz/valinor/src/Library/Settings.php
vendored
@@ -6,6 +6,9 @@
|
||||
|
||||
use Closure;
|
||||
use CuyZ\Valinor\Cache\Cache;
|
||||
use CuyZ\Valinor\Mapper\Http\FromBody;
|
||||
use CuyZ\Valinor\Mapper\Http\FromQuery;
|
||||
use CuyZ\Valinor\Mapper\Http\FromRoute;
|
||||
use CuyZ\Valinor\Mapper\Object\Constructor;
|
||||
use CuyZ\Valinor\Mapper\Object\DynamicConstructor;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
|
||||
@@ -15,8 +18,13 @@
|
||||
use Throwable;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function hash;
|
||||
use function implode;
|
||||
use function krsort;
|
||||
use function serialize;
|
||||
|
||||
/** @internal */
|
||||
final class Settings
|
||||
@@ -57,6 +65,9 @@ final class Settings
|
||||
/** @var array<int, list<callable>> */
|
||||
public array $mapperConverters = [];
|
||||
|
||||
/** @var list<callable(string): string> */
|
||||
public array $keyConverters = [];
|
||||
|
||||
/** @var array<class-string, null> */
|
||||
public array $mapperConverterAttributes = [];
|
||||
|
||||
@@ -85,6 +96,9 @@ public function allowedAttributes(): array
|
||||
return [
|
||||
Constructor::class,
|
||||
DynamicConstructor::class,
|
||||
FromBody::class,
|
||||
FromQuery::class,
|
||||
FromRoute::class,
|
||||
...array_keys($this->mapperConverterAttributes),
|
||||
...array_keys($this->normalizerTransformerAttributes),
|
||||
];
|
||||
@@ -157,6 +171,7 @@ public function callables(): array
|
||||
...$this->customConstructors,
|
||||
...array_merge(...$this->mapperConverters),
|
||||
...array_merge(...$this->normalizerTransformers),
|
||||
...$this->keyConverters,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
use CuyZ\Valinor\Utility\ValueDumper;
|
||||
use RuntimeException;
|
||||
|
||||
use function count;
|
||||
|
||||
/** @internal */
|
||||
final class ArgumentsMapperError extends RuntimeException implements MappingError
|
||||
{
|
||||
|
||||
10
vendor/cuyz/valinor/src/Mapper/AsConverter.php
vendored
10
vendor/cuyz/valinor/src/Mapper/AsConverter.php
vendored
@@ -10,18 +10,16 @@
|
||||
* This attribute can be used to automatically register a converter attribute.
|
||||
*
|
||||
* When there is no control over the transformer attribute class, the following
|
||||
* method can be used: @see \CuyZ\Valinor\MapperBuilder::registerConverter()
|
||||
* method can be used: {@see \CuyZ\Valinor\MapperBuilder::registerConverter()}
|
||||
*
|
||||
* ```php
|
||||
* ```
|
||||
* namespace My\App;
|
||||
*
|
||||
* #[\CuyZ\Valinor\Mapper\AsConverter]
|
||||
* #[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
* final class CastToBool
|
||||
* {
|
||||
* /**
|
||||
* * @param callable(mixed): bool $next
|
||||
* * /
|
||||
* // @param callable(mixed): bool $next
|
||||
* public function map(string $value, callable $next): bool
|
||||
* {
|
||||
* $value = match ($value) {
|
||||
@@ -56,7 +54,7 @@
|
||||
* Attribute converters can also be used on function parameters when mapping
|
||||
* arguments:
|
||||
*
|
||||
* ```php
|
||||
* ```
|
||||
* function someFunction(string $name, #[\My\App\CastToBool] bool $isActive) {
|
||||
* // …
|
||||
* };
|
||||
|
||||
70
vendor/cuyz/valinor/src/Mapper/Configurator/ConvertKeysToCamelCase.php
vendored
Normal file
70
vendor/cuyz/valinor/src/Mapper/Configurator/ConvertKeysToCamelCase.php
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Configurator;
|
||||
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
|
||||
use function lcfirst;
|
||||
use function str_replace;
|
||||
use function ucwords;
|
||||
|
||||
/**
|
||||
* Converts the keys of input data to `camelCase` before mapping them to object
|
||||
* properties or shaped array keys. This allows accepting data with a different
|
||||
* naming convention than the one used in the PHP codebase.
|
||||
*
|
||||
* A typical use case is mapping a JSON API payload that uses `snake_case` keys
|
||||
* to PHP objects that follow the `camelCase` convention:
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase;
|
||||
*
|
||||
* $user = (new MapperBuilder())
|
||||
* ->configureWith(new ConvertKeysToCamelCase())
|
||||
* ->mapper()
|
||||
* ->map(User::class, [
|
||||
* 'first_name' => 'John', // mapped to `$firstName`
|
||||
* 'last_name' => 'Doe', // mapped to `$lastName`
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* This configurator can be combined with a key restriction configurator to both
|
||||
* validate and convert keys in a single step. The restriction configurator must
|
||||
* be registered *before* the conversion so that the validation runs on the
|
||||
* original input keys.
|
||||
*
|
||||
* - {@see RestrictKeysToSnakeCase}
|
||||
* - {@see RestrictKeysToKebabCase}
|
||||
* - {@see RestrictKeysToPascalCase}
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToSnakeCase;
|
||||
*
|
||||
* $user = (new MapperBuilder())
|
||||
* ->configureWith(
|
||||
* new RestrictKeysToSnakeCase(),
|
||||
* new ConvertKeysToCamelCase(),
|
||||
* )
|
||||
* ->mapper()
|
||||
* ->map(User::class, [
|
||||
* 'first_name' => 'John',
|
||||
* 'last_name' => 'Doe',
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class ConvertKeysToCamelCase implements MapperBuilderConfigurator
|
||||
{
|
||||
public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
|
||||
{
|
||||
return $builder->registerKeyConverter(
|
||||
static fn (string $key): string => lcfirst(str_replace(['_', '-'], '', ucwords($key, '_-'))), // @phpstan-ignore argument.type
|
||||
);
|
||||
}
|
||||
}
|
||||
68
vendor/cuyz/valinor/src/Mapper/Configurator/ConvertKeysToSnakeCase.php
vendored
Normal file
68
vendor/cuyz/valinor/src/Mapper/Configurator/ConvertKeysToSnakeCase.php
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Configurator;
|
||||
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
|
||||
use function ltrim;
|
||||
use function preg_replace;
|
||||
use function str_replace;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* Converts the keys of input data to `snake_case` before mapping them to
|
||||
* object properties or shaped array keys. This allows accepting data with a
|
||||
* different naming convention than the one used in the PHP codebase.
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToSnakeCase;
|
||||
*
|
||||
* $user = (new MapperBuilder())
|
||||
* ->configureWith(new ConvertKeysToSnakeCase())
|
||||
* ->mapper()
|
||||
* ->map(User::class, [
|
||||
* 'firstName' => 'John', // mapped to `$first_name`
|
||||
* 'lastName' => 'Doe', // mapped to `$last_name`
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* This configurator can be combined with a key restriction configurator to both
|
||||
* validate and convert keys in a single step. The restriction configurator must
|
||||
* be registered *before* the conversion so that the validation runs on the
|
||||
* original input keys.
|
||||
*
|
||||
* - {@see RestrictKeysToCamelCase}
|
||||
* - {@see RestrictKeysToKebabCase}
|
||||
* - {@see RestrictKeysToPascalCase}
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToSnakeCase;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToCamelCase;
|
||||
*
|
||||
* $user = (new MapperBuilder())
|
||||
* ->configureWith(
|
||||
* new RestrictKeysToCamelCase(),
|
||||
* new ConvertKeysToSnakeCase(),
|
||||
* )
|
||||
* ->mapper()
|
||||
* ->map(User::class, [
|
||||
* 'firstName' => 'John',
|
||||
* 'lastName' => 'Doe',
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class ConvertKeysToSnakeCase implements MapperBuilderConfigurator
|
||||
{
|
||||
public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
|
||||
{
|
||||
return $builder->registerKeyConverter(
|
||||
static fn (string $key): string => ltrim(strtolower((string)preg_replace('/[A-Z]/', '_$0', str_replace('-', '_', $key))), '_') // @phpstan-ignore argument.type
|
||||
);
|
||||
}
|
||||
}
|
||||
47
vendor/cuyz/valinor/src/Mapper/Configurator/MapperBuilderConfigurator.php
vendored
Normal file
47
vendor/cuyz/valinor/src/Mapper/Configurator/MapperBuilderConfigurator.php
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Configurator;
|
||||
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
|
||||
/**
|
||||
* Allows applying reusable and shareable configuration logic to a
|
||||
* {@see MapperBuilder} instance.
|
||||
*
|
||||
* This is useful when the same mapping configuration needs to be applied in
|
||||
* multiple places across an application, or when configuration logic needs to
|
||||
* be distributed as a package.
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\MapperBuilderConfigurator;
|
||||
*
|
||||
* final class ApplicationMappingConfigurator implements MapperBuilderConfigurator
|
||||
* {
|
||||
* public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
|
||||
* {
|
||||
* return $builder
|
||||
* ->allowSuperfluousKeys()
|
||||
* ->registerConstructor(
|
||||
* \App\Domain\CustomerId::fromString(...),
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // The same configurator can be reused across multiple mappers
|
||||
* $result = (new MapperBuilder())
|
||||
* ->configureWith(new ApplicationMappingConfigurator())
|
||||
* ->mapper()
|
||||
* ->map(SomeClass::class, [
|
||||
* // …
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
interface MapperBuilderConfigurator
|
||||
{
|
||||
public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder;
|
||||
}
|
||||
46
vendor/cuyz/valinor/src/Mapper/Configurator/RestrictKeysToCamelCase.php
vendored
Normal file
46
vendor/cuyz/valinor/src/Mapper/Configurator/RestrictKeysToCamelCase.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Configurator;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
/**
|
||||
* Restricts input keys to `camelCase` format when mapping data to objects or
|
||||
* shaped arrays. If a key does not match, a mapping error will be raised.
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToCamelCase;
|
||||
*
|
||||
* $user = (new MapperBuilder())
|
||||
* ->configureWith(new RestrictKeysToCamelCase())
|
||||
* ->mapper()
|
||||
* ->map(User::class, [
|
||||
* 'firstName' => 'John', // Ok
|
||||
* 'last_name' => 'Doe', // Error
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class RestrictKeysToCamelCase implements MapperBuilderConfigurator
|
||||
{
|
||||
public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
|
||||
{
|
||||
return $builder->registerKeyConverter(
|
||||
// @phpstan-ignore argument.type
|
||||
static function (string $key): string {
|
||||
if (preg_match('/^[a-z][a-zA-Z0-9]*$/', $key) === 0) {
|
||||
throw MessageBuilder::newError('Key must follow the camelCase format.')->withCode('invalid_key_case')->build();
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
46
vendor/cuyz/valinor/src/Mapper/Configurator/RestrictKeysToKebabCase.php
vendored
Normal file
46
vendor/cuyz/valinor/src/Mapper/Configurator/RestrictKeysToKebabCase.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Configurator;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
/**
|
||||
* Restricts input keys to `kebab-case` format when mapping data to objects or
|
||||
* shaped arrays. If a key does not match, a mapping error will be raised.
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToKebabCase;
|
||||
*
|
||||
* $user = (new MapperBuilder())
|
||||
* ->configureWith(new RestrictKeysToKebabCase())
|
||||
* ->mapper()
|
||||
* ->map(User::class, [
|
||||
* 'first-name' => 'John', // Ok
|
||||
* 'last_name' => 'Doe', // Error
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class RestrictKeysToKebabCase implements MapperBuilderConfigurator
|
||||
{
|
||||
public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
|
||||
{
|
||||
return $builder->registerKeyConverter(
|
||||
// @phpstan-ignore argument.type
|
||||
static function (string $key): string {
|
||||
if (preg_match('/^[a-z0-9-]*$/', $key) === 0) {
|
||||
throw MessageBuilder::newError('Key must follow the kebab-case format.')->withCode('invalid_key_case')->build();
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
46
vendor/cuyz/valinor/src/Mapper/Configurator/RestrictKeysToPascalCase.php
vendored
Normal file
46
vendor/cuyz/valinor/src/Mapper/Configurator/RestrictKeysToPascalCase.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Configurator;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
/**
|
||||
* Restricts input keys to `PascalCase` format when mapping data to objects or
|
||||
* shaped arrays. If a key does not match, a mapping error will be raised.
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToPascalCase;
|
||||
*
|
||||
* $user = (new MapperBuilder())
|
||||
* ->configureWith(new RestrictKeysToPascalCase())
|
||||
* ->mapper()
|
||||
* ->map(User::class, [
|
||||
* 'FirstName' => 'John', // Ok
|
||||
* 'last_name' => 'Doe', // Error
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class RestrictKeysToPascalCase implements MapperBuilderConfigurator
|
||||
{
|
||||
public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
|
||||
{
|
||||
return $builder->registerKeyConverter(
|
||||
// @phpstan-ignore argument.type
|
||||
static function (string $key): string {
|
||||
if (preg_match('/^[A-Z][a-zA-Z0-9]*$/', $key) === 0) {
|
||||
throw MessageBuilder::newError('Key must follow the PascalCase format.')->withCode('invalid_key_case')->build();
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
46
vendor/cuyz/valinor/src/Mapper/Configurator/RestrictKeysToSnakeCase.php
vendored
Normal file
46
vendor/cuyz/valinor/src/Mapper/Configurator/RestrictKeysToSnakeCase.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Configurator;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
/**
|
||||
* Restricts input keys to `snake_case` format when mapping data to objects or
|
||||
* shaped arrays. If a key does not match, a mapping error will be raised.
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
* use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToSnakeCase;
|
||||
*
|
||||
* $user = (new MapperBuilder())
|
||||
* ->configureWith(new RestrictKeysToSnakeCase())
|
||||
* ->mapper()
|
||||
* ->map(User::class, [
|
||||
* 'first_name' => 'John', // Ok
|
||||
* 'lastName' => 'Doe', // Error
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class RestrictKeysToSnakeCase implements MapperBuilderConfigurator
|
||||
{
|
||||
public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
|
||||
{
|
||||
return $builder->registerKeyConverter(
|
||||
// @phpstan-ignore argument.type
|
||||
static function (string $key): string {
|
||||
if (preg_match('/^[a-z0-9_]*$/', $key) === 0) {
|
||||
throw MessageBuilder::newError('Key must follow the snake_case format.')->withCode('invalid_key_case')->build();
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
10
vendor/cuyz/valinor/src/Mapper/Exception/MappingLogicalException.php
vendored
Normal file
10
vendor/cuyz/valinor/src/Mapper/Exception/MappingLogicalException.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/** @internal */
|
||||
interface MappingLogicalException extends Throwable {}
|
||||
16
vendor/cuyz/valinor/src/Mapper/Exception/PsrRequestParsedBodyIsObject.php
vendored
Normal file
16
vendor/cuyz/valinor/src/Mapper/Exception/PsrRequestParsedBodyIsObject.php
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** @internal */
|
||||
final class PsrRequestParsedBodyIsObject extends RuntimeException
|
||||
{
|
||||
public function __construct(object $parsedBody)
|
||||
{
|
||||
parent::__construct('HTTP request\'s body must be an array, got an instance of `' . $parsedBody::class . '`.');
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
namespace CuyZ\Valinor\Mapper\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\UnresolvableShellType;
|
||||
use LogicException;
|
||||
|
||||
use function lcfirst;
|
||||
@@ -13,7 +12,7 @@
|
||||
/** @internal */
|
||||
final class TypeErrorDuringArgumentsMapping extends LogicException
|
||||
{
|
||||
public function __construct(FunctionDefinition $function, UnresolvableShellType $exception)
|
||||
public function __construct(FunctionDefinition $function, MappingLogicalException $exception)
|
||||
{
|
||||
parent::__construct(
|
||||
"Could not map arguments of `$function->signature`: " . lcfirst($exception->getMessage()),
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Exception;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\UnresolvableShellType;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use LogicException;
|
||||
|
||||
@@ -13,7 +12,7 @@
|
||||
/** @internal */
|
||||
final class TypeErrorDuringMapping extends LogicException
|
||||
{
|
||||
public function __construct(Type $type, UnresolvableShellType $exception)
|
||||
public function __construct(Type $type, MappingLogicalException $exception)
|
||||
{
|
||||
parent::__construct(
|
||||
"Error while trying to map to `{$type->toString()}`: " . lcfirst($exception->getMessage()),
|
||||
|
||||
34
vendor/cuyz/valinor/src/Mapper/Http/FromBody.php
vendored
Normal file
34
vendor/cuyz/valinor/src/Mapper/Http/FromBody.php
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Http;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Marks a parameter or property to be mapped from body values of an HTTP
|
||||
* request.
|
||||
*
|
||||
* By default, each parameter marked with this attribute will be mapped from a
|
||||
* single body value with the same name.
|
||||
*
|
||||
* For more information, {@see HttpRequest}.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
|
||||
final readonly class FromBody
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* When set to `true`, the entire body values are mapped to this single
|
||||
* parameter.
|
||||
*
|
||||
* This is particularly useful when a lot of values are expected, and it
|
||||
* is preferred to map them to an object instead of individual
|
||||
* parameters.
|
||||
*/
|
||||
public bool $asRoot = false,
|
||||
) {}
|
||||
}
|
||||
34
vendor/cuyz/valinor/src/Mapper/Http/FromQuery.php
vendored
Normal file
34
vendor/cuyz/valinor/src/Mapper/Http/FromQuery.php
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Http;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Marks a parameter or property to be mapped from query parameters of an HTTP
|
||||
* request.
|
||||
*
|
||||
* By default, each parameter marked with this attribute will be mapped from a
|
||||
* single query parameter with the same name.
|
||||
*
|
||||
* For more information, {@see HttpRequest}.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
|
||||
final readonly class FromQuery
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* When set to `true`, the entire query values are mapped to this single
|
||||
* parameter.
|
||||
*
|
||||
* This is particularly useful when a lot of values are expected, and it
|
||||
* is preferred to map them to an object instead of individual
|
||||
* parameters.
|
||||
*/
|
||||
public bool $asRoot = false,
|
||||
) {}
|
||||
}
|
||||
18
vendor/cuyz/valinor/src/Mapper/Http/FromRoute.php
vendored
Normal file
18
vendor/cuyz/valinor/src/Mapper/Http/FromRoute.php
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Http;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Marks a parameter or property to be mapped from route values of an HTTP
|
||||
* request.
|
||||
*
|
||||
* For more information, {@see HttpRequest}.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
|
||||
final class FromRoute {}
|
||||
269
vendor/cuyz/valinor/src/Mapper/Http/HttpRequest.php
vendored
Normal file
269
vendor/cuyz/valinor/src/Mapper/Http/HttpRequest.php
vendored
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Http;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Exception\PsrRequestParsedBodyIsObject;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
use function is_object;
|
||||
|
||||
/**
|
||||
* This class represents an HTTP request that can be given to the mapper to have
|
||||
* custom mapping rules applied, based on the request's data.
|
||||
*
|
||||
* An HTTP request can be built directly from a PSR-7 request:
|
||||
*
|
||||
* ```
|
||||
* $request = \CuyZ\Valinor\Mapper\Http\HttpRequest::fromPsr(
|
||||
* $psrRequest, // PSR-7 `ServerRequestInterface` instance
|
||||
* $routeParameters, // Results from a router
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* Parameters can be mapped from route, query and body values.
|
||||
*
|
||||
* Three attributes are available to explicitly bind a parameter to a single
|
||||
* source, ensuring the value is never resolved from the wrong source:
|
||||
*
|
||||
* - `#[FromRoute]` — for parameters extracted from the URL path by a router
|
||||
* - `#[FromQuery]` — for query string parameters
|
||||
* - `#[FromBody]` — for request body values
|
||||
*
|
||||
* Those attributes can be omitted entirely if the parameter is not bound to a
|
||||
* specific source, in which case a collision error is raised if the same key is
|
||||
* found in more than one source.
|
||||
*
|
||||
* This gives controllers a clean, type-safe signature without coupling to a
|
||||
* framework's request object, while benefiting from the library's validation
|
||||
* and error handling.
|
||||
*
|
||||
* Normal mapping rules apply there: parameters are required unless they have a
|
||||
* default value.
|
||||
*
|
||||
* Route and query parameter values coming from an HTTP request are typically
|
||||
* strings. The mapper automatically handles scalar value casting for these
|
||||
* parameters: a string `"42"` will be properly mapped to an `int` parameter.
|
||||
*
|
||||
* Mapping a request using attributes
|
||||
* ==================================
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\Mapper\Http\FromQuery;
|
||||
* use CuyZ\Valinor\Mapper\Http\FromRoute;
|
||||
* use CuyZ\Valinor\Mapper\Http\HttpRequest;
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
*
|
||||
* final class ListArticles
|
||||
* {
|
||||
* /**
|
||||
* * GET /api/authors/{authorId}/articles?status=X&sort=X&page=X&limit=X
|
||||
* *
|
||||
* * @param positive-int $page
|
||||
* * @param int<10, 100> $limit
|
||||
* * /
|
||||
* public function __invoke(
|
||||
* // Comes from the route
|
||||
* #[FromRoute] string $authorId,
|
||||
*
|
||||
* // All come from query parameters
|
||||
* #[FromQuery] string $status,
|
||||
* #[FromQuery] string $sort,
|
||||
* #[FromQuery] int $page = 1,
|
||||
* #[FromQuery] int $limit = 10,
|
||||
* ): ResponseInterface { … }
|
||||
* }
|
||||
*
|
||||
* // GET /api/authors/42/articles?status=published&sort=date-desc&page=2
|
||||
* $request = new HttpRequest(
|
||||
* routeParameters: ['authorId' => 42],
|
||||
* queryParameters: [
|
||||
* 'status' => 'published',
|
||||
* 'sort' => 'date-desc',
|
||||
* 'page' => 2,
|
||||
* ],
|
||||
* );
|
||||
*
|
||||
* $controller = new ListArticles();
|
||||
*
|
||||
* $arguments = (new MapperBuilder())
|
||||
* ->argumentsMapper()
|
||||
* ->mapArguments($controller, $request);
|
||||
*
|
||||
* $response = $controller(...$arguments);
|
||||
* ```
|
||||
*
|
||||
* Mapping a request without using attributes
|
||||
* ==========================================
|
||||
*
|
||||
* When it is unnecessary to distinguish which source a parameter comes from,
|
||||
* the attribute can be omitted entirely — the mapper will resolve each
|
||||
* parameter from whichever source contains the matching key.
|
||||
* ```
|
||||
* use CuyZ\Valinor\Mapper\Http\FromBody;
|
||||
* use CuyZ\Valinor\Mapper\Http\FromRoute;
|
||||
* use CuyZ\Valinor\Mapper\Http\HttpRequest;
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
*
|
||||
* final class PostComment
|
||||
* {
|
||||
* /**
|
||||
* * POST /api/posts/{postId}/comments
|
||||
* *
|
||||
* * @param positive-int $postId
|
||||
* * @param non-empty-string $author
|
||||
* * @param non-empty-string $content
|
||||
* * /
|
||||
* public function __invoke(
|
||||
* int $postId,
|
||||
* string $author,
|
||||
* string $content,
|
||||
* ): ResponseInterface { … }
|
||||
* }
|
||||
*
|
||||
* // POST /api/posts/1337/comments
|
||||
* $request = new HttpRequest(
|
||||
* routeParameters: ['postId' => 1337],
|
||||
* bodyValues: [
|
||||
* 'author' => 'jane.doe@example.com',
|
||||
* 'content' => 'Great article, thanks for sharing!',
|
||||
* ],
|
||||
* );
|
||||
*
|
||||
* $controller = new PostComment();
|
||||
*
|
||||
* $arguments = (new MapperBuilder())
|
||||
* ->argumentsMapper()
|
||||
* ->mapArguments($controller, $request);
|
||||
*
|
||||
* $response = $controller(...$arguments);
|
||||
* ```
|
||||
*
|
||||
* Flattening query/body parameters
|
||||
* ================================
|
||||
*
|
||||
* Instead of mapping individual query parameters or body values to separate
|
||||
* parameters, the `asRoot` parameter can be used to map all of them at once to
|
||||
* a single parameter. This is useful when working with complex data structures
|
||||
* or when the number of parameters is large.
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\Mapper\Http\FromQuery;
|
||||
* use CuyZ\Valinor\Mapper\Http\FromRoute;
|
||||
*
|
||||
* final readonly class ArticleFilters
|
||||
* {
|
||||
* public function __construct(
|
||||
* public string $status,
|
||||
* public string $sort,
|
||||
* /** @var positive-int * /
|
||||
* public int $page = 1,
|
||||
* /** @var int<10, 100> * /
|
||||
* public int $limit = 10,
|
||||
* ) {}
|
||||
* }
|
||||
*
|
||||
* final class ListArticles
|
||||
* {
|
||||
* // GET /api/authors/{authorId}/articles?status=X&sort=X&page=X&limit=X
|
||||
* public function __invoke(
|
||||
* #[FromRoute] string $authorId,
|
||||
* #[FromQuery(asRoot: true)] ArticleFilters $filters,
|
||||
* ): ResponseInterface { … }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The same approach works with `#[FromBody(asRoot: true)]` for body values.
|
||||
*
|
||||
* Mapping to an object
|
||||
* ====================
|
||||
*
|
||||
* Instead of mapping to a callable's arguments, an `HttpRequest` can be mapped
|
||||
* directly to an object. The attributes work the same way on constructor
|
||||
* parameters or promoted properties.
|
||||
*
|
||||
* ```
|
||||
* use CuyZ\Valinor\Mapper\Http\FromBody;
|
||||
* use CuyZ\Valinor\Mapper\Http\FromRoute;
|
||||
* use CuyZ\Valinor\Mapper\Http\HttpRequest;
|
||||
* use CuyZ\Valinor\MapperBuilder;
|
||||
*
|
||||
* final readonly class PostComment
|
||||
* {
|
||||
* public function __construct(
|
||||
* #[FromRoute] public int $postId,
|
||||
* /** @var non-empty-string * /
|
||||
* #[FromBody] public string $author,
|
||||
* /** @var non-empty-string * /
|
||||
* #[FromBody] public string $content,
|
||||
* ) {}
|
||||
* }
|
||||
*
|
||||
* $request = new HttpRequest(
|
||||
* routeParameters: ['postId' => 1337],
|
||||
* bodyValues: [
|
||||
* 'author' => 'jane.doe@example.com',
|
||||
* 'content' => 'Great article, thanks for sharing!',
|
||||
* ],
|
||||
* );
|
||||
*
|
||||
* $comment = (new MapperBuilder())
|
||||
* ->mapper()
|
||||
* ->map(PostComment::class, $request);
|
||||
*
|
||||
* // $comment->postId === 1337
|
||||
* // $comment->author === 'jane.doe@example.com'
|
||||
* // $comment->content === 'Great article, thanks for sharing!'
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final readonly class HttpRequest
|
||||
{
|
||||
/** @pure */
|
||||
public function __construct(
|
||||
/**
|
||||
* Route parameters that were extracted by the router.
|
||||
*
|
||||
* @var array<mixed>
|
||||
*/
|
||||
public array $routeParameters = [],
|
||||
|
||||
/**
|
||||
* Query parameters that were extracted from the request URI.
|
||||
*
|
||||
* @var array<mixed>
|
||||
*/
|
||||
public array $queryParameters = [],
|
||||
|
||||
/**
|
||||
* Body values that were extracted from the request content.
|
||||
*
|
||||
* @var array<mixed>
|
||||
*/
|
||||
public array $bodyValues = [],
|
||||
|
||||
/**
|
||||
* Original request object coming, for instance, from a library or a
|
||||
* framework. If it is given, then this object will automatically be
|
||||
* mapped to any target parameter matching its type.
|
||||
*/
|
||||
public ?object $requestObject = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
* @param array<mixed> $routeParameters
|
||||
*/
|
||||
public static function fromPsr(ServerRequestInterface $request, array $routeParameters = []): self
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
if (is_object($body)) {
|
||||
throw new PsrRequestParsedBodyIsObject($body);
|
||||
}
|
||||
|
||||
return new self($routeParameters, $request->getQueryParams(), $body ?? [], $request);
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,15 @@
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use Countable;
|
||||
use CuyZ\Valinor\Definition\ParameterDefinition;
|
||||
use CuyZ\Valinor\Definition\Parameters;
|
||||
use CuyZ\Valinor\Definition\Properties;
|
||||
use CuyZ\Valinor\Definition\PropertyDefinition;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayElement;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayType;
|
||||
use CuyZ\Valinor\Type\Types\StringValueType;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
use function array_diff_key;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
@@ -25,10 +24,10 @@
|
||||
*
|
||||
* @implements IteratorAggregate<Argument>
|
||||
*/
|
||||
final class Arguments implements IteratorAggregate, Countable
|
||||
final readonly class Arguments implements IteratorAggregate, Countable
|
||||
{
|
||||
/** @var array<string, Argument> */
|
||||
private readonly array $arguments;
|
||||
private array $arguments;
|
||||
|
||||
private ShapedArrayType $shapedArray;
|
||||
|
||||
@@ -39,12 +38,25 @@ public function __construct(Argument ...$arguments)
|
||||
$args[$argument->name()] = $argument;
|
||||
}
|
||||
$this->arguments = $args;
|
||||
$this->shapedArray = new ShapedArrayType(
|
||||
elements: array_map(
|
||||
static fn (Argument $argument) => new ShapedArrayElement(
|
||||
key: new StringValueType($argument->name()),
|
||||
type: $argument->type(),
|
||||
optional: ! $argument->isRequired(),
|
||||
attributes: $argument->attributes(),
|
||||
),
|
||||
$this->arguments,
|
||||
),
|
||||
isUnsealed: false,
|
||||
unsealedType: null,
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromParameters(Parameters $parameters): self
|
||||
{
|
||||
return new self(...array_map(
|
||||
fn (ParameterDefinition $parameter) => Argument::fromParameter($parameter),
|
||||
Argument::fromParameter(...),
|
||||
[...$parameters],
|
||||
));
|
||||
}
|
||||
@@ -52,7 +64,7 @@ public static function fromParameters(Parameters $parameters): self
|
||||
public static function fromProperties(Properties $properties): self
|
||||
{
|
||||
return new self(...array_map(
|
||||
fn (PropertyDefinition $property) => Argument::fromProperty($property),
|
||||
Argument::fromProperty(...),
|
||||
[...$properties],
|
||||
));
|
||||
}
|
||||
@@ -80,19 +92,7 @@ public function merge(self $other): self
|
||||
|
||||
public function toShapedArray(): ShapedArrayType
|
||||
{
|
||||
return $this->shapedArray ??= new ShapedArrayType(
|
||||
elements: array_map(
|
||||
static fn (Argument $argument) => new ShapedArrayElement(
|
||||
key: new StringValueType($argument->name()),
|
||||
type: $argument->type(),
|
||||
optional: ! $argument->isRequired(),
|
||||
attributes: $argument->attributes(),
|
||||
),
|
||||
$this->arguments,
|
||||
),
|
||||
isUnsealed: false,
|
||||
unsealedType: null,
|
||||
);
|
||||
return $this->shapedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,19 +4,25 @@
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Http\HttpRequest;
|
||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||
use CuyZ\Valinor\Type\CompositeTraversableType;
|
||||
use CuyZ\Valinor\Type\Types\ArrayKeyType;
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
|
||||
use function array_filter;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function is_iterable;
|
||||
use function iterator_to_array;
|
||||
|
||||
/** @internal */
|
||||
final class ArgumentsValues
|
||||
final readonly class ArgumentsValues
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Shell $shell,
|
||||
public Shell $shell,
|
||||
private string|null $singleArgumentName = null,
|
||||
) {}
|
||||
|
||||
@@ -40,7 +46,7 @@ public static function forInterface(Shell $shell, Arguments $arguments): self
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```php
|
||||
* ```
|
||||
* final readonly class User
|
||||
* {
|
||||
* public function __construct(
|
||||
@@ -62,6 +68,10 @@ public static function forClass(Shell $shell, Arguments $arguments): self
|
||||
$shell = $shell->withValue(iterator_to_array($shell->value()));
|
||||
}
|
||||
|
||||
if ($shell->value() instanceof HttpRequest) {
|
||||
return new self($shell->withType($arguments->toShapedArray()));
|
||||
}
|
||||
|
||||
if (count($arguments) !== 1) {
|
||||
return new self($shell->withType($arguments->toShapedArray()));
|
||||
}
|
||||
@@ -71,16 +81,13 @@ public static function forClass(Shell $shell, Arguments $arguments): self
|
||||
$type = $argument->type();
|
||||
$attributes = $argument->attributes();
|
||||
|
||||
$isTraversableAndAllowsStringKeys = $type instanceof CompositeTraversableType
|
||||
&& $type->keyType() !== ArrayKeyType::integer();
|
||||
|
||||
if (is_array($shell->value()) && array_key_exists($name, $shell->value())) {
|
||||
if (! $isTraversableAndAllowsStringKeys || $shell->allowSuperfluousKeys || count($shell->value()) === 1) {
|
||||
if (! $type instanceof CompositeTraversableType || $shell->allowSuperfluousKeys || count($shell->value()) === 1) {
|
||||
return new self($shell->withType($arguments->toShapedArray()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($shell->value() === [] && ! $isTraversableAndAllowsStringKeys) {
|
||||
if ($shell->value() === [] && ! $type instanceof CompositeTraversableType) {
|
||||
return new self($shell->withType($arguments->toShapedArray()));
|
||||
}
|
||||
|
||||
@@ -89,6 +96,21 @@ public static function forClass(Shell $shell, Arguments $arguments): self
|
||||
// structure to allow the mapper to do its job. Note that the method
|
||||
// `transform()` below allows to get back the desired structure, with
|
||||
// the mapped value.
|
||||
// If the target type is a union type, we purposely remove any subtype
|
||||
// that references the class to prevent an infinite loop due to circular
|
||||
// dependency.
|
||||
if ($type instanceof UnionType) {
|
||||
$subTypes = $type->types();
|
||||
$filtered = array_filter(
|
||||
$subTypes,
|
||||
static fn (Type $subType) => ! $subType instanceof ObjectType || $subType->className() !== $shell->type->className() // @phpstan-ignore method.notFound (We know $shell->type is an ObjectType)
|
||||
);
|
||||
|
||||
if ($filtered !== $subTypes) {
|
||||
$type = UnionType::from(...$filtered);
|
||||
}
|
||||
}
|
||||
|
||||
return new self(
|
||||
shell: $shell->withType($type)->withAttributes($attributes),
|
||||
singleArgumentName: $argument->name(),
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
* of.
|
||||
*
|
||||
* This attribute is a convenient replacement to the usage of the constructor
|
||||
* registration method: @see \CuyZ\Valinor\MapperBuilder::registerConstructor()
|
||||
* registration method:
|
||||
* {@see \CuyZ\Valinor\MapperBuilder::registerConstructor()}
|
||||
*
|
||||
* ```php
|
||||
* ```
|
||||
* final readonly class Email
|
||||
* {
|
||||
* // When another constructor is registered for the class, the native
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* Note that the first parameter of the constructor has to be a string otherwise
|
||||
* an exception will be thrown on mapping.
|
||||
*
|
||||
* ```php
|
||||
* ```
|
||||
* interface SomeInterfaceWithStaticConstructor
|
||||
* {
|
||||
* public static function from(string $value): self;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\HasParameters;
|
||||
use RuntimeException;
|
||||
|
||||
use function implode;
|
||||
|
||||
/** @internal */
|
||||
final class CannotParseToDateTime extends RuntimeException implements ErrorMessage, HasCode, HasParameters
|
||||
{
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object\Exception;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\HasCode;
|
||||
|
||||
/** @internal */
|
||||
final class InvalidSource implements ErrorMessage, HasCode
|
||||
{
|
||||
private string $body;
|
||||
|
||||
private string $code = 'invalid_source';
|
||||
|
||||
public function __construct(mixed $source)
|
||||
{
|
||||
if ($source === null) {
|
||||
$this->body = 'Cannot be empty and must be filled with a value matching {expected_signature}.';
|
||||
} else {
|
||||
$this->body = 'Value {source_value} does not match {expected_signature}.';
|
||||
}
|
||||
}
|
||||
|
||||
public function body(): string
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function code(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,11 @@
|
||||
use CuyZ\Valinor\Type\Types\NativeStringType;
|
||||
use CuyZ\Valinor\Utility\Reflection\Reflection;
|
||||
|
||||
use function array_filter;
|
||||
use function array_key_exists;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_a;
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Utility\TypeHelper;
|
||||
|
||||
use function array_intersect_key;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function krsort;
|
||||
use function usort;
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionObject;
|
||||
use CuyZ\Valinor\Definition\ParameterDefinition;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
use Exception;
|
||||
|
||||
use function array_map;
|
||||
use function array_shift;
|
||||
use function array_values;
|
||||
|
||||
/** @internal */
|
||||
final class FunctionObjectBuilder implements ObjectBuilder
|
||||
@@ -29,7 +29,7 @@ public function __construct(FunctionObject $function, ObjectType $type)
|
||||
$definition = $function->definition;
|
||||
|
||||
$arguments = array_map(
|
||||
fn (ParameterDefinition $parameter) => Argument::fromParameter($parameter),
|
||||
Argument::fromParameter(...),
|
||||
array_values([...$definition->parameters])
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_values;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use BackedEnum;
|
||||
use CuyZ\Valinor\Type\Types\Factory\ValueTypeFactory;
|
||||
use CuyZ\Valinor\Type\Types\EnumType;
|
||||
use CuyZ\Valinor\Type\Types\Factory\ValueTypeFactory;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -4,13 +4,22 @@
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Source\Modifier;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
use function assert;
|
||||
use function is_int;
|
||||
use function is_iterable;
|
||||
use function is_string;
|
||||
use function lcfirst;
|
||||
use function str_replace;
|
||||
use function ucwords;
|
||||
|
||||
/**
|
||||
* @deprecated This modifier will be removed in version 3.0.
|
||||
* Use the configurator {@see ConvertKeysToCamelCase} instead.
|
||||
*
|
||||
* @api
|
||||
* @implements IteratorAggregate<mixed>
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Source\Modifier;
|
||||
|
||||
use function count;
|
||||
|
||||
/** @internal */
|
||||
final class Mapping
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
use function array_filter;
|
||||
use function explode;
|
||||
use function is_array;
|
||||
|
||||
|
||||
@@ -65,7 +65,12 @@ public static function file(SplFileObject $file): Source
|
||||
return new Source(new FileSource($file));
|
||||
}
|
||||
|
||||
/** @pure */
|
||||
/**
|
||||
* @deprecated This modifier will be removed in version 3.0.
|
||||
* Use the configurator {@see ConvertKeysToCamelCase} instead.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function camelCaseKeys(): Source
|
||||
{
|
||||
return new Source(new CamelCaseKeys($this));
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidIterableKeyType;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidArrayKey;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidIterableKeyType;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\SourceIsEmptyArray;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\SourceMustBeIterable;
|
||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||
|
||||
202
vendor/cuyz/valinor/src/Mapper/Tree/Builder/HttpRequestNodeBuilder.php
vendored
Normal file
202
vendor/cuyz/valinor/src/Mapper/Tree/Builder/HttpRequestNodeBuilder.php
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Http\FromBody;
|
||||
use CuyZ\Valinor\Mapper\Http\FromQuery;
|
||||
use CuyZ\Valinor\Mapper\Http\FromRoute;
|
||||
use CuyZ\Valinor\Mapper\Http\HttpRequest;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\CannotMapHttpRequestToUnsealedShapedArray;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\CannotUseBothFromBodyAttributes;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\CannotUseBothFromQueryAttributes;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\HttpRequestKeyCollision;
|
||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayType;
|
||||
use CuyZ\Valinor\Type\Types\UnresolvableType;
|
||||
|
||||
use function array_intersect_key;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
|
||||
/** @internal */
|
||||
final class HttpRequestNodeBuilder implements NodeBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private NodeBuilder $delegate,
|
||||
private KeyConversionPipeline $keyConverterContainer,
|
||||
) {}
|
||||
|
||||
public function build(Shell $shell): Node
|
||||
{
|
||||
$request = $shell->value();
|
||||
$type = $shell->type;
|
||||
|
||||
if (! $type instanceof ShapedArrayType) {
|
||||
return $this->delegate->build($shell);
|
||||
}
|
||||
|
||||
if (! $request instanceof HttpRequest) {
|
||||
return $this->delegate->build($shell);
|
||||
}
|
||||
|
||||
if ($type->isUnsealed) {
|
||||
throw new CannotMapHttpRequestToUnsealedShapedArray();
|
||||
}
|
||||
|
||||
// We always allow superfluous keys: HTTP request are coming from the
|
||||
// outside of the application, meaning extra parameters can be added
|
||||
// anytime. This could lead to server issues like DDoS log spam.
|
||||
$shell = $shell->allowSuperfluousKeys();
|
||||
|
||||
$route = $request->routeParameters;
|
||||
$query = $request->queryParameters;
|
||||
$body = $request->bodyValues;
|
||||
$errors = [];
|
||||
|
||||
if ($this->keyConverterContainer->hasConverters()) {
|
||||
// Key converters (e.g. camelCase to snake_case) are applied to all
|
||||
// three sources independently. Each conversion returns the renamed
|
||||
// values, a name map for error reporting, and any conversion errors.
|
||||
[$route, $routeNameMap, $routeErrors] = $this->keyConverterContainer->convert($request->routeParameters);
|
||||
[$query, $queryNameMap, $queryErrors] = $this->keyConverterContainer->convert($request->queryParameters);
|
||||
[$body, $bodyNameMap, $bodyErrors] = $this->keyConverterContainer->convert($request->bodyValues);
|
||||
|
||||
foreach ([...$routeErrors, ...$queryErrors, ...$bodyErrors] as $key => $error) {
|
||||
$errors[] = $shell->child($key, UnresolvableType::forInvalidKey())->error($error);
|
||||
}
|
||||
|
||||
$shell = $shell->withNameMap([...$routeNameMap, ...$queryNameMap, ...$bodyNameMap]);
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return $shell->errors($errors);
|
||||
}
|
||||
|
||||
$elements = [];
|
||||
$checkCollision = [];
|
||||
$result = [];
|
||||
|
||||
$queryAttributes = 0;
|
||||
$bodyAttributes = 0;
|
||||
$queryasRoot = false;
|
||||
$bodyasRoot = false;
|
||||
|
||||
foreach ($type->elements as $key => $element) {
|
||||
$attributes = $element->attributes();
|
||||
|
||||
if ($attributes->has(FromRoute::class)) {
|
||||
// The value must *NEVER* come from query or body.
|
||||
unset($query[$key], $body[$key]);
|
||||
|
||||
$elements[$key] = $element;
|
||||
} elseif ($attributes->has(FromQuery::class)) {
|
||||
/** @var FromQuery $attribute */
|
||||
$attribute = $attributes->firstOf(FromQuery::class)->instantiate();
|
||||
|
||||
// This element must be resolved exclusively from query
|
||||
// parameters. When `asRoot` is true, the entire query array is
|
||||
// mapped to this single element.
|
||||
if ($attribute->asRoot) {
|
||||
$queryasRoot = true;
|
||||
|
||||
$node = $shell->withType($element->type())->withValue($query)->build();
|
||||
$query = [];
|
||||
|
||||
if ($node->isValid()) {
|
||||
$result[$element->key()->value()] = $node->value();
|
||||
} else {
|
||||
$errors[] = $node;
|
||||
}
|
||||
} else {
|
||||
// The value must *NEVER* come from route or body.
|
||||
unset($route[$key], $body[$key]);
|
||||
|
||||
$elements[$key] = $element;
|
||||
}
|
||||
|
||||
$queryAttributes++;
|
||||
|
||||
// No other `#[FromQuery]` element is allowed alongside.
|
||||
if ($queryasRoot && $queryAttributes > 1) {
|
||||
throw new CannotUseBothFromQueryAttributes();
|
||||
}
|
||||
} elseif ($attributes->has(FromBody::class)) {
|
||||
/** @var FromBody $attribute */
|
||||
$attribute = $attributes->firstOf(FromBody::class)->instantiate();
|
||||
|
||||
// This element must be resolved exclusively from body values.
|
||||
// When `asRoot` is true, the entire body array is mapped to
|
||||
// this single element.
|
||||
if ($attribute->asRoot) {
|
||||
$bodyasRoot = true;
|
||||
|
||||
$node = $shell->withType($element->type())->withValue($body)->build();
|
||||
$body = [];
|
||||
|
||||
if ($node->isValid()) {
|
||||
$result[$element->key()->value()] = $node->value();
|
||||
} else {
|
||||
$errors[] = $node;
|
||||
}
|
||||
} else {
|
||||
// The value must *NEVER* come from route or query.
|
||||
unset($route[$key], $query[$key]);
|
||||
|
||||
$elements[$key] = $element;
|
||||
}
|
||||
|
||||
$bodyAttributes++;
|
||||
|
||||
// No other `#[FromBody]` element is allowed alongside.
|
||||
if ($bodyasRoot && $bodyAttributes > 1) {
|
||||
throw new CannotUseBothFromBodyAttributes();
|
||||
}
|
||||
} elseif ($request->requestObject && $element->type()->accepts($request->requestObject)) {
|
||||
$result[$key] = $request->requestObject;
|
||||
} else {
|
||||
$checkCollision[] = $key;
|
||||
$elements[$key] = $element;
|
||||
}
|
||||
}
|
||||
|
||||
if ($checkCollision !== []) {
|
||||
$collisionErrors = [];
|
||||
$collisions = array_intersect_key($route, $query) + array_intersect_key($route, $body) + array_intersect_key($query, $body);
|
||||
|
||||
foreach ($checkCollision as $key) {
|
||||
if (array_key_exists($key, $collisions)) {
|
||||
$collisionErrors[] = $shell->child($key, UnresolvableType::forInvalidKey())->error(new HttpRequestKeyCollision($key));
|
||||
}
|
||||
}
|
||||
|
||||
if ($collisionErrors !== []) {
|
||||
return $shell->errors($collisionErrors);
|
||||
}
|
||||
}
|
||||
|
||||
if (! $shell->allowScalarValueCasting) {
|
||||
// Route and query values are all string values, so we enable scalar
|
||||
// value casting for them.
|
||||
$shell = $shell->allowScalarValueCastingForChildren(array_keys($route + $query));
|
||||
}
|
||||
|
||||
// Build the remaining elements (those not handled by `asRoot` or
|
||||
// request object injection) using the merged values from all sources.
|
||||
$node = $shell
|
||||
->withType(new ShapedArrayType($elements))
|
||||
->withValue($route + $query + $body)
|
||||
->build();
|
||||
|
||||
if (! $node->isValid()) {
|
||||
$errors[] = $node;
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return $shell->errors($errors);
|
||||
}
|
||||
|
||||
return $shell->node($result + $node->value()); // @phpstan-ignore binaryOp.invalid (we know the node value is an array)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
use Exception;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use function is_string;
|
||||
|
||||
/** @internal */
|
||||
final class InterfaceInferringContainer
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
use CuyZ\Valinor\Utility\Polyfill;
|
||||
use Throwable;
|
||||
|
||||
use function assert;
|
||||
|
||||
/** @internal */
|
||||
final class InterfaceNodeBuilder implements NodeBuilder
|
||||
{
|
||||
|
||||
107
vendor/cuyz/valinor/src/Mapper/Tree/Builder/KeyConversionPipeline.php
vendored
Normal file
107
vendor/cuyz/valinor/src/Mapper/Tree/Builder/KeyConversionPipeline.php
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||
|
||||
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\KeyConverterHasInvalidStringParameter;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\KeyConverterHasNoParameter;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\KeyConverterHasTooManyParameters;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\KeysCollision;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\Message;
|
||||
use CuyZ\Valinor\Type\StringType;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
use function array_key_exists;
|
||||
|
||||
/** @internal */
|
||||
final class KeyConversionPipeline
|
||||
{
|
||||
private bool $convertersCallablesWereChecked = false;
|
||||
|
||||
public function __construct(
|
||||
private FunctionDefinitionRepository $functionDefinitionRepository,
|
||||
/** @var list<callable(string): string> */
|
||||
private array $converters,
|
||||
/** @var callable(Throwable): ErrorMessage */
|
||||
private mixed $exceptionFilter,
|
||||
) {}
|
||||
|
||||
public function hasConverters(): bool
|
||||
{
|
||||
return $this->converters !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $values
|
||||
* @return array{
|
||||
* 0: array<mixed>,
|
||||
* 1: array<string, string>,
|
||||
* 2: array<string, Message>,
|
||||
* }
|
||||
*/
|
||||
public function convert(array $values): array
|
||||
{
|
||||
$this->checkConverterCallables();
|
||||
|
||||
$newValue = [];
|
||||
$nameMap = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
$convertedKey = (string)$key;
|
||||
|
||||
try {
|
||||
foreach ($this->converters as $converter) {
|
||||
$convertedKey = $converter($convertedKey);
|
||||
}
|
||||
|
||||
if (array_key_exists($convertedKey, $nameMap)) {
|
||||
$errors[(string)$key] = new KeysCollision($nameMap[$convertedKey], $convertedKey);
|
||||
} else {
|
||||
$newValue[$convertedKey] = $value;
|
||||
|
||||
if ($convertedKey !== (string)$key) {
|
||||
$nameMap[$convertedKey] = (string)$key;
|
||||
}
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
if (! $exception instanceof Message) {
|
||||
$exception = ($this->exceptionFilter)($exception);
|
||||
}
|
||||
|
||||
$errors[(string)$key] = $exception;
|
||||
}
|
||||
}
|
||||
|
||||
return [$newValue, $nameMap, $errors];
|
||||
}
|
||||
|
||||
private function checkConverterCallables(): void
|
||||
{
|
||||
if ($this->convertersCallablesWereChecked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->convertersCallablesWereChecked = true;
|
||||
|
||||
foreach ($this->converters as $converter) {
|
||||
$function = $this->functionDefinitionRepository->for($converter);
|
||||
|
||||
if ($function->parameters->count() === 0) {
|
||||
throw new KeyConverterHasNoParameter($function);
|
||||
}
|
||||
|
||||
if ($function->parameters->count() > 1) {
|
||||
throw new KeyConverterHasTooManyParameters($function);
|
||||
}
|
||||
|
||||
if (! $function->parameters->at(0)->nativeType instanceof StringType) {
|
||||
throw new KeyConverterHasInvalidStringParameter($function, $function->parameters->at(0)->nativeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
vendor/cuyz/valinor/src/Mapper/Tree/Builder/KeyConverterNodeBuilder.php
vendored
Normal file
70
vendor/cuyz/valinor/src/Mapper/Tree/Builder/KeyConverterNodeBuilder.php
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayType;
|
||||
use CuyZ\Valinor\Type\Types\UnresolvableType;
|
||||
|
||||
use function is_array;
|
||||
use function is_iterable;
|
||||
use function iterator_to_array;
|
||||
|
||||
/** @internal */
|
||||
final class KeyConverterNodeBuilder implements NodeBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private NodeBuilder $delegate,
|
||||
private KeyConversionPipeline $keyConverterContainer,
|
||||
) {}
|
||||
|
||||
public function build(Shell $shell): Node
|
||||
{
|
||||
if (! $this->shouldConvertKeys($shell)) {
|
||||
return $this->delegate->build($shell);
|
||||
}
|
||||
|
||||
$value = $shell->value();
|
||||
|
||||
if (! is_iterable($value)) {
|
||||
return $this->delegate->build($shell);
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
$value = iterator_to_array($value);
|
||||
}
|
||||
|
||||
[$newValue, $nameMap, $keyErrors] = $this->keyConverterContainer->convert($value);
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($keyErrors as $key => $error) {
|
||||
$errors[] = $shell
|
||||
->child($key, UnresolvableType::forInvalidKey())
|
||||
->error($error);
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return $shell->errors($errors);
|
||||
}
|
||||
|
||||
return $this->delegate->build(
|
||||
$shell->withValue($newValue)->withNameMap($nameMap),
|
||||
);
|
||||
}
|
||||
|
||||
private function shouldConvertKeys(Shell $shell): bool
|
||||
{
|
||||
// Keys were already converted by a previous pass through this builder,
|
||||
// so we skip to avoid double-transformation.
|
||||
if ($shell->hasNameMap()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $shell->type instanceof ShapedArrayType
|
||||
|| $shell->type instanceof ObjectType;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
use CuyZ\Valinor\Type\Types\ListType;
|
||||
use CuyZ\Valinor\Type\Types\NonEmptyListType;
|
||||
|
||||
use function array_map;
|
||||
use function assert;
|
||||
use function is_int;
|
||||
use function is_iterable;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||
use CuyZ\Valinor\Type\Types\MixedType;
|
||||
|
||||
use function assert;
|
||||
|
||||
/** @internal */
|
||||
final class MixedNodeBuilder implements NodeBuilder
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ private function __construct(
|
||||
*/
|
||||
public static function new(mixed $value, int $childrenCount): self
|
||||
{
|
||||
return new self(value: $value, childrenCount: $childrenCount);
|
||||
return new self($value, childrenCount: $childrenCount);
|
||||
}
|
||||
|
||||
public static function error(Shell $shell, Message $error): self
|
||||
@@ -42,7 +42,7 @@ public static function error(Shell $shell, Message $error): self
|
||||
$shell->dumpValue(),
|
||||
);
|
||||
|
||||
return new self(value: null, messages: [$nodeMessage]);
|
||||
return new self(null, messages: [$nodeMessage]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +56,7 @@ public static function branchWithErrors(array $nodes): self
|
||||
$messages = array_merge($messages, $node->messages);
|
||||
}
|
||||
|
||||
return new self(value: null, messages: $messages);
|
||||
return new self(null, messages: $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,14 +82,6 @@ public function messages(): array
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
public function appendMessage(NodeMessage $message): self
|
||||
{
|
||||
return new self(
|
||||
value: null,
|
||||
messages: [...$this->messages, $message],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-negative-int
|
||||
*/
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\SourceMustBeIterable;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\UnexpectedKeysInSource;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\NodeMessage;
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\UnexpectedKeyInSource;
|
||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayType;
|
||||
use CuyZ\Valinor\Type\Types\UnresolvableType;
|
||||
|
||||
use function array_diff_key;
|
||||
use function array_key_exists;
|
||||
use function assert;
|
||||
use function is_array;
|
||||
use function is_iterable;
|
||||
use function iterator_to_array;
|
||||
|
||||
/** @internal */
|
||||
final class ShapedArrayNodeBuilder implements NodeBuilder
|
||||
@@ -29,19 +31,19 @@ public function build(Shell $shell): Node
|
||||
return $shell->error(new SourceMustBeIterable($value));
|
||||
}
|
||||
|
||||
$children = [];
|
||||
$childrenNames = [];
|
||||
$errors = [];
|
||||
|
||||
if (! is_array($value)) {
|
||||
$value = iterator_to_array($value);
|
||||
}
|
||||
|
||||
foreach ($type->elements as $key => $element) {
|
||||
$childrenNames[] = $key;
|
||||
$children = [];
|
||||
$errors = [];
|
||||
|
||||
$child = $shell->child((string)$key, $element->type());
|
||||
$child = $child->withAttributes($element->attributes());
|
||||
// First phase: we loop through all the shaped array elements and try
|
||||
// to find corresponding value in the source value to build them.
|
||||
foreach ($type->elements as $key => $element) {
|
||||
$child = $shell
|
||||
->child((string)$key, $element->type())
|
||||
->withAttributes($element->attributes());
|
||||
|
||||
if (array_key_exists($key, $value)) {
|
||||
$child = $child->withValue($value[$key]);
|
||||
@@ -51,72 +53,46 @@ public function build(Shell $shell): Node
|
||||
|
||||
$child = $child->build();
|
||||
|
||||
if (! $child->isValid()) {
|
||||
$errors[] = $child;
|
||||
} else {
|
||||
if ($child->isValid()) {
|
||||
$children[$key] = $child->value();
|
||||
} else {
|
||||
$errors[] = $child;
|
||||
}
|
||||
|
||||
unset($value[$key]);
|
||||
}
|
||||
|
||||
// Second phase: if the shaped array is unsealed, we take the remaining
|
||||
// values from the source and try to build them.
|
||||
if ($type->isUnsealed) {
|
||||
$unsealedNode = $shell
|
||||
->withType($type->unsealedType())
|
||||
->withValue($value)
|
||||
->build();
|
||||
|
||||
if (! $unsealedNode->isValid()) {
|
||||
$errors[] = $unsealedNode;
|
||||
} else {
|
||||
if ($unsealedNode->isValid()) {
|
||||
// @phpstan-ignore assignOp.invalid (we know value is an array)
|
||||
$children += $unsealedNode->value();
|
||||
} else {
|
||||
$errors[] = $unsealedNode;
|
||||
}
|
||||
} elseif (! $shell->allowSuperfluousKeys) {
|
||||
// Third phase: the superfluous keys are not allowed, so we add an
|
||||
// error for each remaining key in the source.
|
||||
$diff = array_diff_key($value, $children, $shell->allowedSuperfluousKeys);
|
||||
|
||||
foreach ($diff as $key => $val) {
|
||||
$errors[] = $shell
|
||||
->child((string)$key, UnresolvableType::forSuperfluousValue())
|
||||
->withValue($val)
|
||||
->error(new UnexpectedKeyInSource());
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
$node = $shell->node($children);
|
||||
} else {
|
||||
$node = $shell->errors($errors);
|
||||
return $shell->node($children);
|
||||
}
|
||||
|
||||
if (! $type->isUnsealed) {
|
||||
$node = $this->checkUnexpectedKeys($shell, $node, $childrenNames);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int|string> $children
|
||||
*/
|
||||
private function checkUnexpectedKeys(Shell $shell, Node $node, array $children): Node
|
||||
{
|
||||
$value = $shell->value();
|
||||
|
||||
if ($shell->allowSuperfluousKeys || ! is_array($value)) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
$diff = array_diff(array_keys($value), $children, $shell->allowedSuperfluousKeys);
|
||||
|
||||
if ($diff === []) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
/** @var non-empty-list<int|string> $children */
|
||||
$error = new UnexpectedKeysInSource($value, $children);
|
||||
|
||||
$nodeMessage = new NodeMessage(
|
||||
$error,
|
||||
$error->body(),
|
||||
$shell->name,
|
||||
$shell->path,
|
||||
"`{$shell->type->toString()}`",
|
||||
$shell->expectedSignature(),
|
||||
$shell->dumpValue(),
|
||||
);
|
||||
|
||||
return $node->appendMessage($nodeMessage);
|
||||
return $shell->errors($errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
use CuyZ\Valinor\Utility\TypeHelper;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use function krsort;
|
||||
use function reset;
|
||||
|
||||
15
vendor/cuyz/valinor/src/Mapper/Tree/Exception/CannotMapHttpRequestToUnsealedShapedArray.php
vendored
Normal file
15
vendor/cuyz/valinor/src/Mapper/Tree/Exception/CannotMapHttpRequestToUnsealedShapedArray.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Exception\MappingLogicalException;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class CannotMapHttpRequestToUnsealedShapedArray extends LogicException implements MappingLogicalException
|
||||
{
|
||||
protected $message = 'Mapping an HTTP request to an unsealed shaped array is not supported.';
|
||||
|
||||
}
|
||||
14
vendor/cuyz/valinor/src/Mapper/Tree/Exception/CannotUseBothFromBodyAttributes.php
vendored
Normal file
14
vendor/cuyz/valinor/src/Mapper/Tree/Exception/CannotUseBothFromBodyAttributes.php
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Exception\MappingLogicalException;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class CannotUseBothFromBodyAttributes extends LogicException implements MappingLogicalException
|
||||
{
|
||||
protected $message = 'Cannot use `#[FromBody(asRoot: true)]` alongside other `#[FromBody]` attributes.';
|
||||
}
|
||||
14
vendor/cuyz/valinor/src/Mapper/Tree/Exception/CannotUseBothFromQueryAttributes.php
vendored
Normal file
14
vendor/cuyz/valinor/src/Mapper/Tree/Exception/CannotUseBothFromQueryAttributes.php
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Exception\MappingLogicalException;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class CannotUseBothFromQueryAttributes extends LogicException implements MappingLogicalException
|
||||
{
|
||||
protected $message = 'Cannot use `#[FromQuery(asRoot: true)]` alongside other `#[FromQuery]` attributes.';
|
||||
}
|
||||
@@ -7,9 +7,10 @@
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class ConverterHasInvalidCallableParameter extends \LogicException
|
||||
final class ConverterHasInvalidCallableParameter extends LogicException
|
||||
{
|
||||
public function __construct(MethodDefinition|FunctionDefinition $method, Type $parameterType)
|
||||
{
|
||||
|
||||
34
vendor/cuyz/valinor/src/Mapper/Tree/Exception/HttpRequestKeyCollision.php
vendored
Normal file
34
vendor/cuyz/valinor/src/Mapper/Tree/Exception/HttpRequestKeyCollision.php
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\HasCode;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\HasParameters;
|
||||
|
||||
/** @internal */
|
||||
final class HttpRequestKeyCollision implements ErrorMessage, HasCode, HasParameters
|
||||
{
|
||||
public function __construct(
|
||||
private string $key,
|
||||
) {}
|
||||
|
||||
public function body(): string
|
||||
{
|
||||
return 'Key `{key}` was found in several HTTP request sources. It must be sent in only one of route, query or body.';
|
||||
}
|
||||
|
||||
public function code(): string
|
||||
{
|
||||
return 'key_collision';
|
||||
}
|
||||
|
||||
public function parameters(): array
|
||||
{
|
||||
return [
|
||||
'key' => $this->key,
|
||||
];
|
||||
}
|
||||
}
|
||||
21
vendor/cuyz/valinor/src/Mapper/Tree/Exception/KeyConverterHasInvalidStringParameter.php
vendored
Normal file
21
vendor/cuyz/valinor/src/Mapper/Tree/Exception/KeyConverterHasInvalidStringParameter.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class KeyConverterHasInvalidStringParameter extends LogicException
|
||||
{
|
||||
public function __construct(MethodDefinition|FunctionDefinition $method, Type $parameterType)
|
||||
{
|
||||
parent::__construct(
|
||||
"Key converter's parameter must be a string, `{$parameterType->toString()}` given for `$method->signature`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
20
vendor/cuyz/valinor/src/Mapper/Tree/Exception/KeyConverterHasNoParameter.php
vendored
Normal file
20
vendor/cuyz/valinor/src/Mapper/Tree/Exception/KeyConverterHasNoParameter.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use RuntimeException;
|
||||
|
||||
/** @internal */
|
||||
final class KeyConverterHasNoParameter extends RuntimeException
|
||||
{
|
||||
public function __construct(MethodDefinition|FunctionDefinition $function)
|
||||
{
|
||||
parent::__construct(
|
||||
"The key converter `$function->signature` has no parameter to convert the key, a string parameter is required.",
|
||||
);
|
||||
}
|
||||
}
|
||||
20
vendor/cuyz/valinor/src/Mapper/Tree/Exception/KeyConverterHasTooManyParameters.php
vendored
Normal file
20
vendor/cuyz/valinor/src/Mapper/Tree/Exception/KeyConverterHasTooManyParameters.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class KeyConverterHasTooManyParameters extends LogicException
|
||||
{
|
||||
public function __construct(MethodDefinition|FunctionDefinition $method)
|
||||
{
|
||||
parent::__construct(
|
||||
"Key converter must have only one parameter, {$method->parameters->count()} given for `$method->signature`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user