allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
This commit is contained in:
47
vendor/cuyz/valinor/src/Normalizer/ArrayNormalizer.php
vendored
Normal file
47
vendor/cuyz/valinor/src/Normalizer/ArrayNormalizer.php
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer;
|
||||
|
||||
use CuyZ\Valinor\Normalizer\Transformer\EmptyObject;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Transformer;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @implements Normalizer<array<mixed>|scalar|null>
|
||||
*/
|
||||
final class ArrayNormalizer implements Normalizer
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(
|
||||
private Transformer $transformer,
|
||||
) {}
|
||||
|
||||
/** @pure */
|
||||
public function normalize(mixed $value): mixed
|
||||
{
|
||||
/** @var array<mixed>|scalar|null */
|
||||
return $this->format(
|
||||
$this->transformer->transform($value),
|
||||
);
|
||||
}
|
||||
|
||||
private function format(mixed $value): mixed
|
||||
{
|
||||
if (is_iterable($value)) {
|
||||
if (! is_array($value)) {
|
||||
$value = iterator_to_array($value);
|
||||
}
|
||||
|
||||
$value = array_map($this->format(...), $value);
|
||||
} elseif ($value instanceof EmptyObject) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
55
vendor/cuyz/valinor/src/Normalizer/AsTransformer.php
vendored
Normal file
55
vendor/cuyz/valinor/src/Normalizer/AsTransformer.php
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* This attribute can be used to automatically register a transformer attribute.
|
||||
*
|
||||
* When there is no control over the transformer attribute class, the following
|
||||
* method can be used: @see \CuyZ\Valinor\NormalizerBuilder::registerTransformer
|
||||
*
|
||||
* ```php
|
||||
* namespace My\App;
|
||||
*
|
||||
* #[\CuyZ\Valinor\Normalizer\AsTransformer]
|
||||
* #[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
* final class DateTimeFormat
|
||||
* {
|
||||
* public function __construct(private string $format) {}
|
||||
*
|
||||
* public function normalize(\DateTimeInterface $date): string
|
||||
* {
|
||||
* return $date->format($this->format);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* final readonly class Event
|
||||
* {
|
||||
* public function __construct(
|
||||
* public string $eventName,
|
||||
* #[\My\App\DateTimeFormat('Y/m/d')]
|
||||
* public \DateTimeInterface $date,
|
||||
* ) {}
|
||||
* }
|
||||
*
|
||||
* (new \CuyZ\Valinor\NormalizerBuilder())
|
||||
* ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
|
||||
* ->normalize(new \My\App\Event(
|
||||
* eventName: 'Release of legendary album',
|
||||
* date: new \DateTimeImmutable('1971-11-08'),
|
||||
* ));
|
||||
*
|
||||
* // [
|
||||
* // 'eventName' => 'Release of legendary album',
|
||||
* // 'date' => '1971/11/08',
|
||||
* // ]
|
||||
* ```
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
final class AsTransformer {}
|
||||
20
vendor/cuyz/valinor/src/Normalizer/Exception/CircularReferenceFoundDuringNormalization.php
vendored
Normal file
20
vendor/cuyz/valinor/src/Normalizer/Exception/CircularReferenceFoundDuringNormalization.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** @internal */
|
||||
final class CircularReferenceFoundDuringNormalization extends RuntimeException
|
||||
{
|
||||
public function __construct(object $object)
|
||||
{
|
||||
$class = $object::class;
|
||||
|
||||
parent::__construct(
|
||||
"A circular reference was detected with an object of type `$class`. Circular references are not supported by the normalizer.",
|
||||
);
|
||||
}
|
||||
}
|
||||
19
vendor/cuyz/valinor/src/Normalizer/Exception/KeyTransformerHasTooManyParameters.php
vendored
Normal file
19
vendor/cuyz/valinor/src/Normalizer/Exception/KeyTransformerHasTooManyParameters.php
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class KeyTransformerHasTooManyParameters extends LogicException
|
||||
{
|
||||
public function __construct(MethodDefinition $method)
|
||||
{
|
||||
parent::__construct(
|
||||
"Key transformer must have at most 1 parameter, {$method->parameters->count()} given for `$method->signature`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
19
vendor/cuyz/valinor/src/Normalizer/Exception/KeyTransformerParameterInvalidType.php
vendored
Normal file
19
vendor/cuyz/valinor/src/Normalizer/Exception/KeyTransformerParameterInvalidType.php
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class KeyTransformerParameterInvalidType extends LogicException
|
||||
{
|
||||
public function __construct(MethodDefinition $method)
|
||||
{
|
||||
parent::__construct(
|
||||
"Key transformer parameter must be a string, {$method->parameters->at(0)->type->toString()} given for `$method->signature`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
21
vendor/cuyz/valinor/src/Normalizer/Exception/TransformerHasInvalidCallableParameter.php
vendored
Normal file
21
vendor/cuyz/valinor/src/Normalizer/Exception/TransformerHasInvalidCallableParameter.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class TransformerHasInvalidCallableParameter extends LogicException
|
||||
{
|
||||
public function __construct(MethodDefinition|FunctionDefinition $method, Type $parameterType)
|
||||
{
|
||||
parent::__construct(
|
||||
"Transformer's second parameter must be a callable, `{$parameterType->toString()}` given for `$method->signature`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
18
vendor/cuyz/valinor/src/Normalizer/Exception/TransformerHasNoParameter.php
vendored
Normal file
18
vendor/cuyz/valinor/src/Normalizer/Exception/TransformerHasNoParameter.php
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class TransformerHasNoParameter extends LogicException
|
||||
{
|
||||
public function __construct(MethodDefinition|FunctionDefinition $method)
|
||||
{
|
||||
parent::__construct("Transformer must have at least one parameter, none given for `$method->signature`.");
|
||||
}
|
||||
}
|
||||
20
vendor/cuyz/valinor/src/Normalizer/Exception/TransformerHasTooManyParameters.php
vendored
Normal file
20
vendor/cuyz/valinor/src/Normalizer/Exception/TransformerHasTooManyParameters.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class TransformerHasTooManyParameters extends LogicException
|
||||
{
|
||||
public function __construct(MethodDefinition|FunctionDefinition $method)
|
||||
{
|
||||
parent::__construct(
|
||||
"Transformer must have at most 2 parameters, {$method->parameters->count()} given for `$method->signature`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
20
vendor/cuyz/valinor/src/Normalizer/Exception/TypeUnhandledByNormalizer.php
vendored
Normal file
20
vendor/cuyz/valinor/src/Normalizer/Exception/TypeUnhandledByNormalizer.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use function get_debug_type;
|
||||
|
||||
/** @internal */
|
||||
final class TypeUnhandledByNormalizer extends RuntimeException
|
||||
{
|
||||
public function __construct(mixed $value)
|
||||
{
|
||||
$type = get_debug_type($value);
|
||||
|
||||
parent::__construct("Value of type `$type` cannot be normalized.");
|
||||
}
|
||||
}
|
||||
101
vendor/cuyz/valinor/src/Normalizer/Format.php
vendored
Normal file
101
vendor/cuyz/valinor/src/Normalizer/Format.php
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @template T of Normalizer
|
||||
*/
|
||||
final class Format
|
||||
{
|
||||
/**
|
||||
* Allows a normalizer to format an input to a PHP array, containing only
|
||||
* scalar values.
|
||||
*
|
||||
* ```php
|
||||
* namespace My\App;
|
||||
*
|
||||
* $normalizer = (new \CuyZ\Valinor\NormalizerBuilder())
|
||||
* ->normalizer(\CuyZ\Valinor\Normalizer\Format::array());
|
||||
*
|
||||
* $userAsArray = $normalizer->normalize(
|
||||
* new \My\App\User(
|
||||
* name: 'John Doe',
|
||||
* age: 42,
|
||||
* country: new \My\App\Country(
|
||||
* name: 'France',
|
||||
* countryCode: 'FR',
|
||||
* ),
|
||||
* )
|
||||
* );
|
||||
*
|
||||
* // `$userAsArray` is now an array and can be manipulated much more easily, for
|
||||
* // instance to be serialized to the wanted data format.
|
||||
* //
|
||||
* // [
|
||||
* // 'name' => 'John Doe',
|
||||
* // 'age' => 42,
|
||||
* // 'country' => [
|
||||
* // 'name' => 'France',
|
||||
* // 'countryCode' => 'FR',
|
||||
* // ],
|
||||
* // ];
|
||||
* ```
|
||||
*
|
||||
* @pure
|
||||
* @return self<ArrayNormalizer>
|
||||
*/
|
||||
public static function array(): self
|
||||
{
|
||||
return new self(ArrayNormalizer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a normalizer to format an input to JSON syntax.
|
||||
*
|
||||
* ```php
|
||||
* namespace My\App;
|
||||
*
|
||||
* $normalizer = (new \CuyZ\Valinor\NormalizerBuilder())
|
||||
* ->normalizer(\CuyZ\Valinor\Normalizer\Format::json());
|
||||
*
|
||||
* $userAsJson = $normalizer->normalize(
|
||||
* new \My\App\User(
|
||||
* name: 'John Doe',
|
||||
* age: 42,
|
||||
* country: new \My\App\Country(
|
||||
* name: 'France',
|
||||
* code: 'FR',
|
||||
* ),
|
||||
* )
|
||||
* );
|
||||
*
|
||||
* // `$userAsJson` is a valid JSON string representing the data:
|
||||
* // {"name":"John Doe","age":42,"country":{"name":"France","code":"FR"}}
|
||||
* ```
|
||||
*
|
||||
* @pure
|
||||
* @return self<JsonNormalizer>
|
||||
*/
|
||||
public static function json(): self
|
||||
{
|
||||
return new self(JsonNormalizer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<T> $type
|
||||
*/
|
||||
private function __construct(private string $type) {}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
* @return class-string<T>
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
18
vendor/cuyz/valinor/src/Normalizer/Formatter/Exception/CannotFormatInvalidTypeToJson.php
vendored
Normal file
18
vendor/cuyz/valinor/src/Normalizer/Formatter/Exception/CannotFormatInvalidTypeToJson.php
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Formatter\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** @internal */
|
||||
final class CannotFormatInvalidTypeToJson extends RuntimeException
|
||||
{
|
||||
public function __construct(mixed $value)
|
||||
{
|
||||
$type = get_debug_type($value);
|
||||
|
||||
parent::__construct("Value of type `$type` cannot be normalized to JSON.");
|
||||
}
|
||||
}
|
||||
138
vendor/cuyz/valinor/src/Normalizer/Formatter/JsonFormatter.php
vendored
Normal file
138
vendor/cuyz/valinor/src/Normalizer/Formatter/JsonFormatter.php
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Formatter;
|
||||
|
||||
use CuyZ\Valinor\Normalizer\Formatter\Exception\CannotFormatInvalidTypeToJson;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\EmptyObject;
|
||||
use Generator;
|
||||
|
||||
use function array_is_list;
|
||||
use function assert;
|
||||
use function fwrite;
|
||||
use function is_bool;
|
||||
use function is_iterable;
|
||||
use function is_null;
|
||||
use function is_scalar;
|
||||
use function json_encode;
|
||||
|
||||
use const JSON_FORCE_OBJECT;
|
||||
|
||||
/** @internal */
|
||||
final class JsonFormatter
|
||||
{
|
||||
private bool $prettyPrint;
|
||||
|
||||
private bool $forceObject;
|
||||
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private readonly mixed $resource,
|
||||
private readonly int $jsonEncodingOptions,
|
||||
) {
|
||||
$this->prettyPrint = (bool)($this->jsonEncodingOptions & JSON_PRETTY_PRINT);
|
||||
$this->forceObject = (bool)($this->jsonEncodingOptions & JSON_FORCE_OBJECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource
|
||||
*/
|
||||
public function format(mixed $value): mixed
|
||||
{
|
||||
$this->formatRecursively($value, 1);
|
||||
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
private function formatRecursively(mixed $value, int $depth): void
|
||||
{
|
||||
if (is_null($value)) {
|
||||
$this->write('null');
|
||||
} elseif (is_bool($value)) {
|
||||
$this->write($value ? 'true' : 'false');
|
||||
} elseif (is_scalar($value)) {
|
||||
/**
|
||||
* @phpstan-ignore-next-line / Due to the new json encoding options feature, it is not possible to let SA
|
||||
* tools understand that JSON_THROW_ON_ERROR is always set.
|
||||
*/
|
||||
$this->write(json_encode($value, $this->jsonEncodingOptions));
|
||||
} elseif ($value instanceof EmptyObject) {
|
||||
$this->write('{}');
|
||||
} elseif (is_iterable($value)) {
|
||||
// Note: when a generator is formatted, it is considered as a list
|
||||
// if its first key is 0. This is done early because the first JSON
|
||||
// character for an array differs from the one for an object, and we
|
||||
// need to know that before actually looping on the generator.
|
||||
//
|
||||
// For generators having a first key of 0 and inconsistent keys
|
||||
// afterward, this leads to a JSON array being written, while it
|
||||
// should have been an object. This is a trade-off we accept,
|
||||
// considering most generators starting at 0 are actually lists.
|
||||
if ($this->forceObject) {
|
||||
$isList = false;
|
||||
} elseif ($value instanceof Generator) {
|
||||
if (! $value->valid()) {
|
||||
$this->write('[]');
|
||||
return;
|
||||
}
|
||||
|
||||
$isList = $value->key() === 0;
|
||||
} else {
|
||||
/** @var array<mixed> $value At this point we know this is an array */
|
||||
$isList = array_is_list($value);
|
||||
}
|
||||
|
||||
$isFirst = true;
|
||||
|
||||
$this->write($isList ? '[' : '{');
|
||||
|
||||
foreach ($value as $key => $val) {
|
||||
$chunk = '';
|
||||
|
||||
if (! $isFirst) {
|
||||
$chunk = ',';
|
||||
}
|
||||
|
||||
if ($this->prettyPrint) {
|
||||
$chunk .= PHP_EOL . str_repeat(' ', $depth);
|
||||
}
|
||||
|
||||
$isFirst = false;
|
||||
|
||||
if (! $isList) {
|
||||
assert(is_scalar($key));
|
||||
|
||||
$key = json_encode((string)$key, $this->jsonEncodingOptions);
|
||||
|
||||
$chunk .= $key . ':';
|
||||
|
||||
if ($this->prettyPrint) {
|
||||
$chunk .= ' ';
|
||||
}
|
||||
}
|
||||
|
||||
$this->write($chunk);
|
||||
|
||||
$this->formatRecursively($val, $depth + 1);
|
||||
}
|
||||
|
||||
$chunk = '';
|
||||
|
||||
if ($this->prettyPrint) {
|
||||
$chunk = PHP_EOL . str_repeat(' ', $depth - 1);
|
||||
}
|
||||
|
||||
$chunk .= $isList ? ']' : '}';
|
||||
|
||||
$this->write($chunk);
|
||||
} else {
|
||||
throw new CannotFormatInvalidTypeToJson($value);
|
||||
}
|
||||
}
|
||||
|
||||
private function write(string $content): void
|
||||
{
|
||||
fwrite($this->resource, $content);
|
||||
}
|
||||
}
|
||||
165
vendor/cuyz/valinor/src/Normalizer/JsonNormalizer.php
vendored
Normal file
165
vendor/cuyz/valinor/src/Normalizer/JsonNormalizer.php
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer;
|
||||
|
||||
use CuyZ\Valinor\Normalizer\Formatter\JsonFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Transformer;
|
||||
use RuntimeException;
|
||||
|
||||
use function get_debug_type;
|
||||
use function is_resource;
|
||||
|
||||
use const JSON_FORCE_OBJECT;
|
||||
use const JSON_HEX_AMP;
|
||||
use const JSON_HEX_APOS;
|
||||
use const JSON_HEX_QUOT;
|
||||
use const JSON_HEX_TAG;
|
||||
use const JSON_INVALID_UTF8_IGNORE;
|
||||
use const JSON_INVALID_UTF8_SUBSTITUTE;
|
||||
use const JSON_NUMERIC_CHECK;
|
||||
use const JSON_PRESERVE_ZERO_FRACTION;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use const JSON_UNESCAPED_LINE_TERMINATORS;
|
||||
use const JSON_UNESCAPED_SLASHES;
|
||||
use const JSON_UNESCAPED_UNICODE;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @implements Normalizer<string>
|
||||
*/
|
||||
final class JsonNormalizer implements Normalizer
|
||||
{
|
||||
private const ACCEPTABLE_JSON_OPTIONS = JSON_FORCE_OBJECT
|
||||
| JSON_HEX_QUOT
|
||||
| JSON_HEX_TAG
|
||||
| JSON_HEX_AMP
|
||||
| JSON_HEX_APOS
|
||||
| JSON_INVALID_UTF8_IGNORE
|
||||
| JSON_INVALID_UTF8_SUBSTITUTE
|
||||
| JSON_NUMERIC_CHECK
|
||||
| JSON_PRETTY_PRINT
|
||||
| JSON_PRESERVE_ZERO_FRACTION
|
||||
| JSON_UNESCAPED_LINE_TERMINATORS
|
||||
| JSON_UNESCAPED_SLASHES
|
||||
| JSON_UNESCAPED_UNICODE
|
||||
| JSON_THROW_ON_ERROR;
|
||||
|
||||
private Transformer $transformer;
|
||||
|
||||
private int $jsonEncodingOptions;
|
||||
|
||||
/**
|
||||
* Internal note
|
||||
* -------------
|
||||
*
|
||||
* We could use the `int-mask-of<JsonNormalizer::JSON_*>` annotation
|
||||
* to let PHPStan infer the type of the accepted options, but some caveats
|
||||
* were found:
|
||||
* - SA tools are not able to infer that we end up having only accepted
|
||||
* options. Might be fixed with https://github.com/phpstan/phpstan/issues/9384
|
||||
* for PHPStan but Psalm does have some (not all) issues as well.
|
||||
* - Using this annotation provokes *severe* performance issues when
|
||||
* running PHPStan analysis, therefore it is preferable to avoid it.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(
|
||||
Transformer $transformer,
|
||||
int $jsonEncodingOptions = JSON_THROW_ON_ERROR,
|
||||
) {
|
||||
$this->transformer = $transformer;
|
||||
$this->jsonEncodingOptions = (self::ACCEPTABLE_JSON_OPTIONS & $jsonEncodingOptions) | JSON_THROW_ON_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, the JSON normalizer will only use `JSON_THROW_ON_ERROR` to
|
||||
* encode non-boolean scalar values. There might be use-cases where projects
|
||||
* will need flags like `JSON_JSON_PRESERVE_ZERO_FRACTION`.
|
||||
*
|
||||
* This can be achieved by passing these flags to this method:
|
||||
*
|
||||
* ```php
|
||||
* $normalizer = (new \CuyZ\Valinor\NormalizerBuilder())
|
||||
* ->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
|
||||
* ->withOptions(\JSON_PRESERVE_ZERO_FRACTION);
|
||||
*
|
||||
* $lowerManhattanAsJson = $normalizer->normalize(
|
||||
* new \My\App\Coordinates(
|
||||
* longitude: 40.7128,
|
||||
* latitude: -74.0000
|
||||
* )
|
||||
* );
|
||||
*
|
||||
* // `$lowerManhattanAsJson` is a valid JSON string representing the data:
|
||||
* // {"longitude":40.7128,"latitude":-74.0000}
|
||||
* ```
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function withOptions(int $options): self
|
||||
{
|
||||
return new self($this->transformer, $options);
|
||||
}
|
||||
|
||||
/** @pure */
|
||||
public function normalize(mixed $value): string
|
||||
{
|
||||
$result = $this->transformer->transform($value);
|
||||
|
||||
/** @var resource $resource */
|
||||
$resource = fopen('php://memory', 'w');
|
||||
|
||||
$formatter = new JsonFormatter($resource, $this->jsonEncodingOptions);
|
||||
$formatter->format($result);
|
||||
|
||||
rewind($resource);
|
||||
|
||||
/** @var string */
|
||||
$json = stream_get_contents($resource);
|
||||
|
||||
fclose($resource);
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new normalizer that will write the JSON to the given resource
|
||||
* instead of returning a string.
|
||||
*
|
||||
* A benefit of streaming the data to a PHP resource is that it may be more
|
||||
* memory-efficient when using generators — for instance when querying a
|
||||
* database:
|
||||
*
|
||||
* ```php
|
||||
* // In this example, we assume that the result of the query below is a
|
||||
* // generator, every entry will be yielded one by one, instead of
|
||||
* // everything being loaded in memory at once.
|
||||
* $users = $database->execute('SELECT * FROM users');
|
||||
*
|
||||
* $file = fopen('path/to/some_file.json', 'w');
|
||||
*
|
||||
* $normalizer = (new \CuyZ\Valinor\NormalizerBuilder())
|
||||
* ->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
|
||||
* ->streamTo($file);
|
||||
*
|
||||
* // Even if there are thousands of users, memory usage will be kept low
|
||||
* // when writing JSON into the file.
|
||||
* $normalizer->normalize($users);
|
||||
* ```
|
||||
*
|
||||
* @pure
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function streamTo(mixed $resource): StreamNormalizer
|
||||
{
|
||||
// This check is there to help people that do not use static analyzers.
|
||||
if (! is_resource($resource)) {
|
||||
throw new RuntimeException('Expected a valid resource, got ' . get_debug_type($resource));
|
||||
}
|
||||
|
||||
return new StreamNormalizer($this->transformer, new JsonFormatter($resource, $this->jsonEncodingOptions));
|
||||
}
|
||||
}
|
||||
26
vendor/cuyz/valinor/src/Normalizer/Normalizer.php
vendored
Normal file
26
vendor/cuyz/valinor/src/Normalizer/Normalizer.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @template-covariant T
|
||||
*/
|
||||
interface Normalizer
|
||||
{
|
||||
/**
|
||||
* A normalizer is a service that transforms a given input into scalar and
|
||||
* array values, while preserving the original structure.
|
||||
*
|
||||
* This feature can be used to share information with other systems that use
|
||||
* a data format (JSON, CSV, XML, etc.). The normalizer will take care of
|
||||
* recursively transforming the data into a format that can be serialized.
|
||||
*
|
||||
* @pure
|
||||
* @return T
|
||||
*/
|
||||
public function normalize(mixed $value): mixed;
|
||||
}
|
||||
32
vendor/cuyz/valinor/src/Normalizer/StreamNormalizer.php
vendored
Normal file
32
vendor/cuyz/valinor/src/Normalizer/StreamNormalizer.php
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer;
|
||||
|
||||
use CuyZ\Valinor\Normalizer\Formatter\JsonFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Transformer;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @implements Normalizer<resource>
|
||||
*/
|
||||
final class StreamNormalizer implements Normalizer
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(
|
||||
private Transformer $transformer,
|
||||
private JsonFormatter $formatter,
|
||||
) {}
|
||||
|
||||
/** @pure */
|
||||
public function normalize(mixed $value): mixed
|
||||
{
|
||||
$result = $this->transformer->transform($value);
|
||||
|
||||
return $this->formatter->format($result);
|
||||
}
|
||||
}
|
||||
161
vendor/cuyz/valinor/src/Normalizer/Transformer/CompiledTransformer.php
vendored
Normal file
161
vendor/cuyz/valinor/src/Normalizer/Transformer/CompiledTransformer.php
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer;
|
||||
|
||||
use Closure;
|
||||
use CuyZ\Valinor\Cache\Cache;
|
||||
use CuyZ\Valinor\Cache\CacheEntry;
|
||||
use CuyZ\Valinor\Cache\TypeFilesWatcher;
|
||||
use CuyZ\Valinor\Compiler\Compiler;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Exception\TypeUnhandledByNormalizer;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerRootNode;
|
||||
use CuyZ\Valinor\Type\CompositeTraversableType;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\ArrayKeyType;
|
||||
use CuyZ\Valinor\Type\Types\ArrayType;
|
||||
use CuyZ\Valinor\Type\Types\EnumType;
|
||||
use CuyZ\Valinor\Type\Types\Factory\ValueTypeFactory;
|
||||
use CuyZ\Valinor\Type\Types\IterableType;
|
||||
use CuyZ\Valinor\Type\Types\NativeBooleanType;
|
||||
use CuyZ\Valinor\Type\Types\NativeClassType;
|
||||
use CuyZ\Valinor\Type\Types\NativeFloatType;
|
||||
use CuyZ\Valinor\Type\Types\NativeIntegerType;
|
||||
use CuyZ\Valinor\Type\Types\NativeStringType;
|
||||
use CuyZ\Valinor\Type\Types\NonEmptyArrayType;
|
||||
use CuyZ\Valinor\Type\Types\NonEmptyListType;
|
||||
use CuyZ\Valinor\Type\Types\NullType;
|
||||
use Generator;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use UnitEnum;
|
||||
|
||||
use function array_is_list;
|
||||
use function is_array;
|
||||
use function is_iterable;
|
||||
use function is_object;
|
||||
use function is_scalar;
|
||||
|
||||
/** @internal */
|
||||
final class CompiledTransformer implements Transformer
|
||||
{
|
||||
public function __construct(
|
||||
private TransformerDefinitionBuilder $definitionBuilder,
|
||||
private TypeFilesWatcher $typeFilesWatcher,
|
||||
/** @var Cache<Transformer> */
|
||||
private Cache $cache,
|
||||
/** @var list<callable> */
|
||||
private array $transformers,
|
||||
) {}
|
||||
|
||||
public function transform(mixed $value): mixed
|
||||
{
|
||||
$type = $this->inferType($value, isSure: true);
|
||||
|
||||
$key = "transformer-\0" . $type->toString();
|
||||
|
||||
$transformer = $this->cache->get($key, $this->transformers, $this);
|
||||
|
||||
if ($transformer) {
|
||||
return $transformer->transform($value);
|
||||
}
|
||||
|
||||
$code = $this->compileFor($type);
|
||||
$filesToWatch = $this->typeFilesWatcher->for($type);
|
||||
|
||||
$this->cache->set($key, new CacheEntry($code, $filesToWatch));
|
||||
|
||||
$transformer = $this->cache->get($key, $this->transformers, $this);
|
||||
|
||||
assert($transformer instanceof Transformer);
|
||||
|
||||
return $transformer->transform($value);
|
||||
}
|
||||
|
||||
private function compileFor(Type $type): string
|
||||
{
|
||||
$rootNode = new TransformerRootNode($this->definitionBuilder, $type);
|
||||
|
||||
$node = Node::shortClosure($rootNode)
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('transformers', 'array'),
|
||||
Node::parameterDeclaration('delegate', Transformer::class),
|
||||
);
|
||||
|
||||
return (new Compiler())->compile($node)->code();
|
||||
}
|
||||
|
||||
private function inferType(mixed $value, bool $isSure = false): Type
|
||||
{
|
||||
return match (true) {
|
||||
$value instanceof UnitEnum => EnumType::native($value::class),
|
||||
is_object($value) && ! $value instanceof Closure && ! $value instanceof Generator => $this->inferObjectType($value),
|
||||
is_iterable($value) => $this->inferIterableType($value),
|
||||
is_scalar($value) && $isSure => ValueTypeFactory::from($value),
|
||||
is_string($value) => NativeStringType::get(),
|
||||
is_int($value) => NativeIntegerType::get(),
|
||||
is_float($value) => NativeFloatType::get(),
|
||||
is_bool($value) => NativeBooleanType::get(),
|
||||
is_null($value) => NullType::get(),
|
||||
default => throw new TypeUnhandledByNormalizer($value),
|
||||
};
|
||||
}
|
||||
|
||||
private function inferObjectType(object $value): NativeClassType
|
||||
{
|
||||
if (is_iterable($value)) {
|
||||
$iterableType = $this->inferIterableType($value);
|
||||
|
||||
return new NativeClassType($value::class, [$iterableType->subType()]);
|
||||
}
|
||||
|
||||
return new NativeClassType($value::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method contains a strongly opinionated rule: when normalizing an
|
||||
* iterable, we assume that the iterable has a high probability of
|
||||
* containing only one type of value, each iteration matching the type of
|
||||
* the first value.
|
||||
*
|
||||
* This is a trade-off between performance and accuracy: the first value
|
||||
* will always be normalized, so we can safely add its type to the compiling
|
||||
* process. For other values, if their types match the first one, that is a
|
||||
* big performance win; if they don't, the transformer will do its best to
|
||||
* find an optimized way of dealing with it or, by default, fall back to
|
||||
* the delegate transformer.
|
||||
*
|
||||
* @param iterable<mixed> $value
|
||||
*/
|
||||
private function inferIterableType(iterable $value): CompositeTraversableType
|
||||
{
|
||||
if (is_array($value)) {
|
||||
if ($value === []) {
|
||||
return ArrayType::native();
|
||||
}
|
||||
|
||||
$firstValueType = $this->inferType(reset($value));
|
||||
|
||||
if (array_is_list($value)) {
|
||||
return new NonEmptyListType($firstValueType);
|
||||
}
|
||||
|
||||
return new NonEmptyArrayType(ArrayKeyType::default(), $firstValueType);
|
||||
}
|
||||
|
||||
if ($value instanceof IteratorAggregate) {
|
||||
$value = $value->getIterator();
|
||||
}
|
||||
|
||||
if ($value instanceof Iterator && $value->valid()) {
|
||||
$firstValueType = $this->inferType($value->current());
|
||||
|
||||
return new IterableType(ArrayKeyType::default(), $firstValueType);
|
||||
}
|
||||
|
||||
return IterableType::native();
|
||||
}
|
||||
}
|
||||
100
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TransformerDefinition.php
vendored
Normal file
100
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TransformerDefinition.php
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler;
|
||||
|
||||
use CuyZ\Valinor\Definition\AttributeDefinition;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\RegisteredTransformersFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\TypeFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\UnsureTypeFormatter;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\UnresolvableType;
|
||||
|
||||
use function array_reverse;
|
||||
|
||||
/** @internal */
|
||||
final class TransformerDefinition
|
||||
{
|
||||
/**
|
||||
* This flag is used to indicate that we are sure what the type of this
|
||||
* definition is. This is important to ensure that the normalizer will act
|
||||
* correctly, even if the value of a property does not respect the expected
|
||||
* type.
|
||||
*
|
||||
* For instance, in the following case we know that the value is a string:
|
||||
*
|
||||
* ```php
|
||||
* final class SomeClass
|
||||
* {
|
||||
* public string $value;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* However, in the following case, we cannot guarantee the type of the
|
||||
* value, as it may contain any value.
|
||||
*
|
||||
* ```php
|
||||
* final class SomeClass
|
||||
* {
|
||||
* /** @var string *\
|
||||
* public $value;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
private bool $isSure = false;
|
||||
|
||||
/** @var list<AttributeDefinition> */
|
||||
private array $transformerAttributes = [];
|
||||
|
||||
public function __construct(
|
||||
public readonly Type $type,
|
||||
/** @var array<int, Type> */
|
||||
private array $transformerTypes,
|
||||
private TypeFormatter $typeFormatter,
|
||||
) {}
|
||||
|
||||
public function typeFormatter(): TypeFormatter
|
||||
{
|
||||
$typeFormatter = $this->typeFormatter;
|
||||
|
||||
if ($this->transformerTypes !== [] || $this->transformerAttributes !== []) {
|
||||
$typeFormatter = new RegisteredTransformersFormatter(
|
||||
$this->type,
|
||||
$typeFormatter,
|
||||
$this->transformerTypes,
|
||||
$this->transformerAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
if (! $this->isSure && ! $this->type instanceof UnresolvableType) {
|
||||
return new UnsureTypeFormatter($typeFormatter, $this->type);
|
||||
}
|
||||
|
||||
return $typeFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<AttributeDefinition> $transformerAttributes
|
||||
*/
|
||||
public function withTransformerAttributes(array $transformerAttributes): self
|
||||
{
|
||||
$self = clone $this;
|
||||
$self->transformerAttributes = [...$self->transformerAttributes, ...array_reverse($transformerAttributes)];
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
public function hasTransformers(): bool
|
||||
{
|
||||
return $this->transformerTypes !== [] || $this->transformerAttributes !== [];
|
||||
}
|
||||
|
||||
public function markAsSure(): self
|
||||
{
|
||||
$self = clone $this;
|
||||
$self->isSure = true;
|
||||
|
||||
return $self;
|
||||
}
|
||||
}
|
||||
128
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TransformerDefinitionBuilder.php
vendored
Normal file
128
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TransformerDefinitionBuilder.php
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler;
|
||||
|
||||
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
|
||||
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\ClassFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\DateTimeFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\DateTimeZoneFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\EnumFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\InterfaceFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\MixedFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\NullFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\ScalarFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\ShapedArrayFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\StdClassFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\TraversableFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\TypeFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\UnionFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter\UnitEnumFormatter;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\TransformerContainer;
|
||||
use CuyZ\Valinor\Type\ClassType;
|
||||
use CuyZ\Valinor\Type\CompositeTraversableType;
|
||||
use CuyZ\Valinor\Type\ScalarType;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\EnumType;
|
||||
use CuyZ\Valinor\Type\Types\GenericType;
|
||||
use CuyZ\Valinor\Type\Types\InterfaceType;
|
||||
use CuyZ\Valinor\Type\Types\MixedType;
|
||||
use CuyZ\Valinor\Type\Types\NativeClassType;
|
||||
use CuyZ\Valinor\Type\Types\NullType;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayType;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use stdClass;
|
||||
use Traversable;
|
||||
use UnitEnum;
|
||||
|
||||
/** @internal */
|
||||
final class TransformerDefinitionBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private ClassDefinitionRepository $classDefinitionRepository,
|
||||
private FunctionDefinitionRepository $functionDefinitionRepository,
|
||||
private TransformerContainer $transformerContainer,
|
||||
) {}
|
||||
|
||||
public function for(Type $type): TransformerDefinition
|
||||
{
|
||||
$definition = new TransformerDefinition(
|
||||
type: $type,
|
||||
transformerTypes: $this->transformerTypes($type),
|
||||
typeFormatter: $this->typeFormatter($type),
|
||||
);
|
||||
|
||||
if ($type instanceof ClassType) {
|
||||
// A class may have transformer attributes, in which case they are
|
||||
// added to the list of attributes for this definition.
|
||||
$definition = $definition->withTransformerAttributes(
|
||||
$this->classDefinitionRepository
|
||||
->for($type)
|
||||
->attributes
|
||||
->filter(TransformerContainer::filterTransformerAttributes(...))
|
||||
->toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($type instanceof MixedType) {
|
||||
$definition = $definition->markAsSure();
|
||||
}
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Type>
|
||||
*/
|
||||
private function transformerTypes(Type $type): array
|
||||
{
|
||||
// If the type is a union type, we don't need to add the transformer
|
||||
// because it will be added for each subtype anyway. This condition also
|
||||
// prevents the transformers from being applied twice in these cases.
|
||||
if ($type instanceof UnionType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$types = [];
|
||||
|
||||
foreach ($this->transformerContainer->transformers() as $key => $transformer) {
|
||||
$function = $this->functionDefinitionRepository->for($transformer);
|
||||
$transformerType = $function->parameters->at(0)->type;
|
||||
|
||||
if (! $type->matches($transformerType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$types[$key] = $transformerType;
|
||||
}
|
||||
|
||||
return array_reverse($types, preserve_keys: true);
|
||||
}
|
||||
|
||||
private function typeFormatter(Type $type): TypeFormatter
|
||||
{
|
||||
return match (true) {
|
||||
$type instanceof EnumType => new EnumFormatter($type),
|
||||
$type instanceof InterfaceType => new InterfaceFormatter($type),
|
||||
$type instanceof NativeClassType => match (true) {
|
||||
$type->className() === UnitEnum::class => new UnitEnumFormatter(),
|
||||
$type->className() === stdClass::class => new StdClassFormatter(),
|
||||
is_a($type->className(), DateTimeInterface::class, true) => new DateTimeFormatter(),
|
||||
is_a($type->className(), DateTimeZone::class, true) => new DateTimeZoneFormatter(),
|
||||
is_a($type->className(), Traversable::class, true) => new TraversableFormatter($type->generics()[0] ?? MixedType::get()),
|
||||
default => new ClassFormatter($this->classDefinitionRepository->for($type)),
|
||||
},
|
||||
$type instanceof NullType => new NullFormatter(),
|
||||
$type instanceof ScalarType => new ScalarFormatter(),
|
||||
$type instanceof ShapedArrayType => new ShapedArrayFormatter($type),
|
||||
$type instanceof UnionType => new UnionFormatter($type),
|
||||
$type instanceof CompositeTraversableType => new TraversableFormatter($type->subType()),
|
||||
$type instanceof GenericType => $this->typeFormatter($type->innerType),
|
||||
default => new MixedFormatter(),
|
||||
};
|
||||
}
|
||||
}
|
||||
73
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TransformerRootNode.php
vendored
Normal file
73
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TransformerRootNode.php
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Compiler;
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\MethodNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Transformer;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use WeakMap;
|
||||
|
||||
/** @internal */
|
||||
final class TransformerRootNode extends Node
|
||||
{
|
||||
public function __construct(
|
||||
private TransformerDefinitionBuilder $definitionBuilder,
|
||||
private Type $type,
|
||||
) {}
|
||||
|
||||
public function compile(Compiler $compiler): Compiler
|
||||
{
|
||||
$definition = $this->definitionBuilder->for($this->type);
|
||||
$definition = $definition->markAsSure();
|
||||
|
||||
$classNode = $this->transformerClassNode($definition);
|
||||
$classNode = $definition->typeFormatter()->manipulateTransformerClass($classNode, $this->definitionBuilder);
|
||||
|
||||
return $classNode->compile($compiler);
|
||||
}
|
||||
|
||||
private function transformerClassNode(TransformerDefinition $definition): AnonymousClassNode
|
||||
{
|
||||
return Node::anonymousClass()
|
||||
->implements(Transformer::class)
|
||||
->withArguments(
|
||||
Node::variable('transformers'),
|
||||
Node::variable('delegate'),
|
||||
)
|
||||
->withProperties(
|
||||
Node::propertyDeclaration('transformers', 'array'),
|
||||
Node::propertyDeclaration('delegate', Transformer::class),
|
||||
)
|
||||
->withMethods(
|
||||
MethodNode::constructor()
|
||||
->withVisibility('public')
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('transformers', 'array'),
|
||||
Node::parameterDeclaration('delegate', Transformer::class),
|
||||
)
|
||||
->withBody(
|
||||
Node::property('transformers')->assign(Node::variable('transformers'))->asExpression(),
|
||||
Node::property('delegate')->assign(Node::variable('delegate'))->asExpression(),
|
||||
),
|
||||
Node::method('transform')
|
||||
->withVisibility('public')
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', 'mixed'),
|
||||
)
|
||||
->withReturnType('mixed')
|
||||
->withBody(
|
||||
Node::variable('references')->assign(Node::newClass(WeakMap::class))->asExpression(),
|
||||
Node::return(
|
||||
$definition->typeFormatter()->formatValueNode(
|
||||
Node::variable('value'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
212
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/ClassFormatter.php
vendored
Normal file
212
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/ClassFormatter.php
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Library\NewAttributeNode;
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Definition\AttributeDefinition;
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Normalizer\Exception\CircularReferenceFoundDuringNormalization;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\TransformerContainer;
|
||||
use CuyZ\Valinor\Type\ScalarType;
|
||||
use CuyZ\Valinor\Type\Types\MixedType;
|
||||
use CuyZ\Valinor\Type\Types\UnresolvableType;
|
||||
use WeakMap;
|
||||
|
||||
/** @internal */
|
||||
final class ClassFormatter implements TypeFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private ClassDefinition $class,
|
||||
) {}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::this()->callMethod(
|
||||
method: $this->methodName(),
|
||||
arguments: [
|
||||
$valueNode,
|
||||
Node::variable('references'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
$methodName = $this->methodName();
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
// This is a placeholder method to avoid circular references coming from
|
||||
// the class properties.
|
||||
$class = $class->withMethods(Node::method($methodName));
|
||||
|
||||
$valuesNode = $this->valuesNode(Node::variable('value'));
|
||||
|
||||
$nodes = [
|
||||
...$this->checkCircularObjectReference(),
|
||||
Node::variable('values')->assign($valuesNode)->asExpression(),
|
||||
];
|
||||
|
||||
$transformedNodes = [
|
||||
Node::variable('transformed')->assign(Node::array())->asExpression(),
|
||||
];
|
||||
|
||||
$shouldUseTransformedNodes = false;
|
||||
|
||||
foreach ($this->class->properties as $property) {
|
||||
$propertyDefinition = $definitionBuilder->for($property->type);
|
||||
|
||||
if (! $property->nativeType instanceof MixedType) {
|
||||
$propertyDefinition = $propertyDefinition->markAsSure();
|
||||
}
|
||||
|
||||
if (! $property->type instanceof UnresolvableType) {
|
||||
$propertyDefinition = $propertyDefinition->withTransformerAttributes(
|
||||
$property->attributes
|
||||
->filter(TransformerContainer::filterTransformerAttributes(...))
|
||||
->filter(
|
||||
static fn (AttributeDefinition $attribute): bool => $property->type->matches(
|
||||
$attribute->class->methods->get('normalize')->parameters->at(0)->type,
|
||||
),
|
||||
)->toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
$typeFormatter = $propertyDefinition->typeFormatter();
|
||||
|
||||
$keyTransformerAttributes = $property->attributes
|
||||
->filter(TransformerContainer::filterKeyTransformerAttributes(...))
|
||||
->toArray();
|
||||
|
||||
if ($keyTransformerAttributes === []) {
|
||||
$key = Node::value($property->name);
|
||||
} else {
|
||||
$transformedNodes[] = Node::variable('key')->assign(Node::value($property->name))->asExpression();
|
||||
|
||||
foreach ($keyTransformerAttributes as $attribute) {
|
||||
$transformedNodes[] = Node::variable('key')->assign(
|
||||
(new NewAttributeNode($attribute))->wrap()->callMethod(
|
||||
method: 'normalizeKey',
|
||||
arguments: [Node::variable('key')],
|
||||
),
|
||||
)->asExpression();
|
||||
}
|
||||
|
||||
$key = Node::variable('key');
|
||||
}
|
||||
|
||||
$transformedNodes[] = Node::variable('transformed')
|
||||
->key($key)
|
||||
->assign($typeFormatter->formatValueNode(
|
||||
Node::variable('values')->key(Node::value($property->name)),
|
||||
))->asExpression();
|
||||
|
||||
$class = $typeFormatter->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
$shouldUseTransformedNodes = $shouldUseTransformedNodes
|
||||
|| $propertyDefinition->hasTransformers()
|
||||
|| $keyTransformerAttributes !== []
|
||||
|| $property->type instanceof UnresolvableType
|
||||
|| ! $property->nativeType instanceof ScalarType;
|
||||
}
|
||||
|
||||
if ($shouldUseTransformedNodes) {
|
||||
$nodes = [
|
||||
...$nodes,
|
||||
...$transformedNodes,
|
||||
Node::return(Node::variable('transformed'))
|
||||
];
|
||||
} else {
|
||||
$nodes[] = Node::return(Node::variable('values'));
|
||||
}
|
||||
|
||||
return $class->withMethods(
|
||||
Node::method($methodName)
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', str_contains($this->class->name, '@anonymous') ? 'object' : $this->class->name),
|
||||
Node::parameterDeclaration('references', WeakMap::class),
|
||||
)
|
||||
->withReturnType('array')
|
||||
->withBody(...$nodes),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Node>
|
||||
*/
|
||||
private function checkCircularObjectReference(): array
|
||||
{
|
||||
return [
|
||||
Node::if(
|
||||
condition: Node::functionCall('isset', [Node::variable('references')->key(Node::variable('value'))]),
|
||||
body: Node::throw(
|
||||
Node::newClass(CircularReferenceFoundDuringNormalization::class, Node::variable('value')),
|
||||
)->asExpression(),
|
||||
),
|
||||
Node::variable('references')->assign(Node::variable('references')->clone())->asExpression(),
|
||||
Node::variable('references')->key(Node::variable('value'))->assign(Node::variable('value'))->asExpression(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a node responsible for getting the properties' values
|
||||
* of an object.
|
||||
*
|
||||
* First scenario: if all properties are public, we can extract them easily
|
||||
* by accessing the properties directly, resulting in a code like this:
|
||||
*
|
||||
* ```
|
||||
* [
|
||||
* 'someProperty' => $value->someProperty,
|
||||
* 'anotherProperty' => $value->anotherProperty,
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Second scenario: at least one property is protected/private. In this
|
||||
* case, we need to "extract" the properties using `get_object_vars`:
|
||||
*
|
||||
* ```
|
||||
* (fn () => get_object_vars($this))->call($value)
|
||||
* ```
|
||||
*/
|
||||
private function valuesNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
$allPropertiesArePublic = true;
|
||||
$assignments = [];
|
||||
|
||||
foreach ($this->class->properties as $property) {
|
||||
$assignments[$property->name] = $valueNode->access($property->name);
|
||||
|
||||
$allPropertiesArePublic = $allPropertiesArePublic && $property->isPublic;
|
||||
}
|
||||
|
||||
if ($allPropertiesArePublic) {
|
||||
return Node::array($assignments);
|
||||
}
|
||||
|
||||
return Node::shortClosure(
|
||||
return: Node::functionCall(
|
||||
name: 'get_object_vars',
|
||||
arguments: [Node::this()],
|
||||
),
|
||||
)->wrap()->callMethod('call', [$valueNode]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function methodName(): string
|
||||
{
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($this->class->type->toString()));
|
||||
|
||||
return "transform_object_{$slug}_" . hash('crc32', $this->class->type->toString());
|
||||
}
|
||||
}
|
||||
26
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeFormatter.php
vendored
Normal file
26
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeFormatter.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
|
||||
/** @internal */
|
||||
final class DateTimeFormatter implements TypeFormatter
|
||||
{
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return $valueNode->callMethod('format', [
|
||||
Node::value('Y-m-d\\TH:i:s.uP'), // RFC 3339
|
||||
]);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
24
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeZoneFormatter.php
vendored
Normal file
24
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeZoneFormatter.php
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
|
||||
/** @internal */
|
||||
final class DateTimeZoneFormatter implements TypeFormatter
|
||||
{
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return $valueNode->callMethod('getName');
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
32
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/EnumFormatter.php
vendored
Normal file
32
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/EnumFormatter.php
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use BackedEnum;
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Type\Types\EnumType;
|
||||
|
||||
/** @internal */
|
||||
final class EnumFormatter implements TypeFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private EnumType $type,
|
||||
) {}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return is_a($this->type->className(), BackedEnum::class, true)
|
||||
? $valueNode->access('value')
|
||||
: $valueNode->access('name');
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
36
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/InterfaceFormatter.php
vendored
Normal file
36
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/InterfaceFormatter.php
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Type\Types\InterfaceType;
|
||||
use DateTimeInterface;
|
||||
|
||||
/** @internal */
|
||||
final class InterfaceFormatter implements TypeFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private InterfaceType $type,
|
||||
) {}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
if ($this->type->className() === DateTimeInterface::class) {
|
||||
return (new DateTimeFormatter())->formatValueNode($valueNode);
|
||||
}
|
||||
|
||||
return Node::this()
|
||||
->access('delegate')
|
||||
->callMethod('transform', [$valueNode]);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
90
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/MixedFormatter.php
vendored
Normal file
90
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/MixedFormatter.php
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Library\TypeAcceptNode;
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Type\Types\IterableType;
|
||||
use CuyZ\Valinor\Type\Types\NativeBooleanType;
|
||||
use CuyZ\Valinor\Type\Types\NativeClassType;
|
||||
use CuyZ\Valinor\Type\Types\NativeFloatType;
|
||||
use CuyZ\Valinor\Type\Types\NativeIntegerType;
|
||||
use CuyZ\Valinor\Type\Types\NativeStringType;
|
||||
use CuyZ\Valinor\Type\Types\NullType;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use UnitEnum;
|
||||
use WeakMap;
|
||||
|
||||
/** @internal */
|
||||
final class MixedFormatter implements TypeFormatter
|
||||
{
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::this()->callMethod(
|
||||
method: 'transform_mixed',
|
||||
arguments: [
|
||||
$valueNode,
|
||||
Node::variable('references'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
if ($class->hasMethod('transform_mixed')) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
// This is a placeholder method to avoid infinite loops.
|
||||
$class = $class->withMethods(Node::method('transform_mixed'));
|
||||
|
||||
$nodes = [];
|
||||
|
||||
$types = [
|
||||
NativeBooleanType::get(),
|
||||
NativeFloatType::get(),
|
||||
NativeIntegerType::get(),
|
||||
NativeStringType::get(),
|
||||
NullType::get(),
|
||||
new NativeClassType(UnitEnum::class),
|
||||
new NativeClassType(DateTime::class),
|
||||
new NativeClassType(DateTimeZone::class),
|
||||
IterableType::native(),
|
||||
];
|
||||
|
||||
foreach ($types as $type) {
|
||||
$definition = $definitionBuilder->for($type)->markAsSure();
|
||||
|
||||
$class = $definition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
$nodes[] = Node::if(
|
||||
condition: new TypeAcceptNode(Node::variable('value'), $definition->type),
|
||||
body: Node::return($definition->typeFormatter()->formatValueNode(Node::variable('value'))),
|
||||
);
|
||||
}
|
||||
|
||||
$nodes[] = Node::return(
|
||||
Node::this()
|
||||
->access('delegate')
|
||||
->callMethod('transform', [
|
||||
Node::variable('value'),
|
||||
]),
|
||||
);
|
||||
|
||||
return $class->withMethods(
|
||||
Node::method('transform_mixed')
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', 'mixed'),
|
||||
Node::parameterDeclaration('references', WeakMap::class),
|
||||
)
|
||||
->withReturnType('mixed')
|
||||
->withBody(...$nodes),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/NullFormatter.php
vendored
Normal file
24
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/NullFormatter.php
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
|
||||
/** @internal */
|
||||
final class NullFormatter implements TypeFormatter
|
||||
{
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::value(null);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Library\NewAttributeNode;
|
||||
use CuyZ\Valinor\Compiler\Library\TypeAcceptNode;
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Definition\AttributeDefinition;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use WeakMap;
|
||||
|
||||
/** @internal */
|
||||
final class RegisteredTransformersFormatter implements TypeFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private Type $type,
|
||||
private TypeFormatter $delegate,
|
||||
/** @var array<int, Type> */
|
||||
private array $transformerTypes,
|
||||
/** @var list<AttributeDefinition> */
|
||||
private array $transformerAttributes = [],
|
||||
) {}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::this()->callMethod(
|
||||
method: $this->methodName(),
|
||||
arguments: [
|
||||
$valueNode,
|
||||
Node::variable('references'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
$methodName = $this->methodName();
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
$class = $this->delegate->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
$nodes = [
|
||||
Node::variable('next')->assign(
|
||||
Node::shortClosure(
|
||||
$this->delegate->formatValueNode(Node::variable('value')),
|
||||
),
|
||||
)->asExpression(),
|
||||
|
||||
...array_map(
|
||||
fn (int $key, Type $transformerType) => Node::if(
|
||||
condition: new TypeAcceptNode(Node::variable('value'), $transformerType),
|
||||
body: Node::variable('next')->assign(
|
||||
Node::shortClosure(
|
||||
return: Node::this()
|
||||
->access('transformers')
|
||||
->key(Node::value($key))
|
||||
->call(arguments: [
|
||||
Node::variable('value'),
|
||||
Node::variable('next'),
|
||||
]),
|
||||
),
|
||||
)->asExpression(),
|
||||
),
|
||||
array_keys($this->transformerTypes),
|
||||
$this->transformerTypes,
|
||||
),
|
||||
|
||||
...array_map(
|
||||
fn (AttributeDefinition $attribute) => Node::variable('next')->assign(
|
||||
Node::shortClosure(
|
||||
return: (new NewAttributeNode($attribute))
|
||||
->wrap()
|
||||
->callMethod(
|
||||
method: 'normalize',
|
||||
arguments: [
|
||||
Node::variable('value'),
|
||||
Node::variable('next'),
|
||||
],
|
||||
),
|
||||
),
|
||||
)->asExpression(),
|
||||
$this->transformerAttributes,
|
||||
),
|
||||
|
||||
Node::return(
|
||||
Node::variable('next')->call(),
|
||||
),
|
||||
];
|
||||
|
||||
return $class->withMethods(
|
||||
Node::method($methodName)
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', 'mixed'),
|
||||
Node::parameterDeclaration('references', WeakMap::class),
|
||||
)
|
||||
->withReturnType('mixed')
|
||||
->withBody(...$nodes),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function methodName(): string
|
||||
{
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($this->type->toString()));
|
||||
|
||||
return "transform_{$slug}_" . sha1(serialize([$this->type->toString(), $this->transformerAttributes]));
|
||||
}
|
||||
}
|
||||
24
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/ScalarFormatter.php
vendored
Normal file
24
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/ScalarFormatter.php
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
|
||||
/** @internal */
|
||||
final class ScalarFormatter implements TypeFormatter
|
||||
{
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return $valueNode;
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
102
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/ShapedArrayFormatter.php
vendored
Normal file
102
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/ShapedArrayFormatter.php
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Type\Types\ArrayType;
|
||||
use CuyZ\Valinor\Type\Types\MixedType;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayType;
|
||||
use WeakMap;
|
||||
|
||||
/** @internal */
|
||||
final class ShapedArrayFormatter implements TypeFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private ShapedArrayType $type,
|
||||
) {}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::this()->callMethod(
|
||||
method: $this->methodName(),
|
||||
arguments: [
|
||||
$valueNode,
|
||||
Node::variable('references'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
$methodName = $this->methodName();
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
if ($this->type->isUnsealed && $this->type->unsealedType() instanceof ArrayType) {
|
||||
$defaultDefinition = $definitionBuilder->for($this->type->unsealedType()->subType());
|
||||
} else {
|
||||
$defaultDefinition = $definitionBuilder->for(MixedType::get());
|
||||
}
|
||||
|
||||
$class = $defaultDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
$elementsDefinitions = [];
|
||||
|
||||
foreach ($this->type->elements as $key => $element) {
|
||||
$elementDefinition = $definitionBuilder->for($element->type());
|
||||
|
||||
$class = $elementDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
$elementsDefinitions[$key] = $elementDefinition;
|
||||
}
|
||||
|
||||
return $class->withMethods(
|
||||
Node::method($methodName)
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', 'array'),
|
||||
Node::parameterDeclaration('references', WeakMap::class),
|
||||
)
|
||||
->withReturnType('array')
|
||||
->withBody(
|
||||
Node::variable('result')->assign(Node::array())->asExpression(),
|
||||
Node::forEach(
|
||||
value: Node::variable('value'),
|
||||
key: 'key',
|
||||
item: 'item',
|
||||
body: Node::variable('result')->key(Node::variable('key'))->assign(
|
||||
(function () use ($defaultDefinition, $elementsDefinitions) {
|
||||
$match = Node::match(Node::variable('key'));
|
||||
|
||||
foreach ($elementsDefinitions as $name => $definition) {
|
||||
$match = $match->withCase(
|
||||
condition: Node::value($name),
|
||||
body: $definition->typeFormatter()->formatValueNode(Node::variable('item')),
|
||||
);
|
||||
}
|
||||
|
||||
return $match->withDefaultCase(
|
||||
$defaultDefinition->typeFormatter()->formatValueNode(Node::variable('item')),
|
||||
);
|
||||
})(),
|
||||
)->asExpression(),
|
||||
),
|
||||
Node::return(Node::variable('result')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function methodName(): string
|
||||
{
|
||||
return 'transform_shaped_array_' . hash('crc32', $this->type->toString());
|
||||
}
|
||||
}
|
||||
68
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/StdClassFormatter.php
vendored
Normal file
68
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/StdClassFormatter.php
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\EmptyObject;
|
||||
use CuyZ\Valinor\Type\Types\MixedType;
|
||||
use stdClass;
|
||||
|
||||
/** @internal */
|
||||
final class StdClassFormatter implements TypeFormatter
|
||||
{
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
if ($class->hasMethod('transform_stdclass')) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
$defaultDefinition = $definitionBuilder->for(MixedType::get());
|
||||
|
||||
$class = $defaultDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
return $class->withMethods(
|
||||
Node::method('transform_stdclass')
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', stdClass::class),
|
||||
Node::parameterDeclaration('references', \WeakMap::class),
|
||||
)
|
||||
->withReturnType('mixed')
|
||||
->withBody(
|
||||
Node::variable('values')->assign(Node::variable('value')->castToArray())->asExpression(),
|
||||
Node::if(
|
||||
condition: Node::variable('values')->equals(Node::array()),
|
||||
body: Node::return(
|
||||
Node::class(EmptyObject::class)->callStaticMethod('get'),
|
||||
),
|
||||
),
|
||||
Node::return(
|
||||
Node::functionCall(
|
||||
name: 'array_map',
|
||||
arguments: [
|
||||
Node::shortClosure(
|
||||
return: $defaultDefinition->typeFormatter()->formatValueNode(Node::variable('value')),
|
||||
)->witParameters(Node::parameterDeclaration('value', 'mixed')),
|
||||
Node::variable('value')->castToArray(),
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::this()->callMethod(
|
||||
method: 'transform_stdclass',
|
||||
arguments: [
|
||||
$valueNode,
|
||||
Node::variable('references'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
114
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/TraversableFormatter.php
vendored
Normal file
114
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/TraversableFormatter.php
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use WeakMap;
|
||||
|
||||
/** @internal */
|
||||
final class TraversableFormatter implements TypeFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private Type $subType,
|
||||
) {}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::this()->callMethod(
|
||||
method: $this->methodName(),
|
||||
arguments: [
|
||||
$valueNode,
|
||||
Node::variable('references'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the input is an array, we use `array_map` to format all sub-values
|
||||
* easily. If the input is not an array, we return a generator that will
|
||||
* yield all transformed values one at a time.
|
||||
*
|
||||
* Generated code should look like:
|
||||
*
|
||||
* ```php
|
||||
* if (is_array($value)) {
|
||||
* return array_map(
|
||||
* fn ($item) => $this->some_function($item),
|
||||
* $value,
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* return (function () use ($value) {
|
||||
* foreach ($value as $key => $item) {
|
||||
* yield $key => $this->some_function($item);
|
||||
* }
|
||||
* })();
|
||||
* ```
|
||||
*/
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
$methodName = $this->methodName();
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
$subDefinition = $definitionBuilder->for($this->subType);
|
||||
|
||||
$class = $subDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
return $class->withMethods(
|
||||
Node::method($methodName)
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', 'iterable'),
|
||||
Node::parameterDeclaration('references', WeakMap::class),
|
||||
)
|
||||
->withReturnType('iterable')
|
||||
->withBody(
|
||||
Node::if(
|
||||
condition: Node::functionCall('is_array', [Node::variable('value')]),
|
||||
body: Node::return(
|
||||
Node::functionCall(
|
||||
name: 'array_map',
|
||||
arguments: [
|
||||
Node::shortClosure(
|
||||
return: $subDefinition->typeFormatter()->formatValueNode(Node::variable('item')),
|
||||
)->witParameters(Node::parameterDeclaration('item', 'mixed')),
|
||||
Node::variable('value')
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
Node::return(
|
||||
Node::closure(
|
||||
Node::forEach(
|
||||
value: Node::variable('value'),
|
||||
key: 'key',
|
||||
item: 'item',
|
||||
body: Node::yield(
|
||||
key: Node::variable('key'),
|
||||
value: $subDefinition->typeFormatter()->formatValueNode(Node::variable('item')),
|
||||
)->asExpression(),
|
||||
)
|
||||
)->uses('value', 'references')->wrap()->call(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function methodName(): string
|
||||
{
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($this->subType->toString()));
|
||||
|
||||
return "transform_iterable_{$slug}_" . hash('crc32', $this->subType->toString());
|
||||
}
|
||||
}
|
||||
18
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/TypeFormatter.php
vendored
Normal file
18
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/TypeFormatter.php
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
|
||||
/** @internal */
|
||||
interface TypeFormatter
|
||||
{
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node;
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode;
|
||||
}
|
||||
83
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/UnionFormatter.php
vendored
Normal file
83
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/UnionFormatter.php
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Library\TypeAcceptNode;
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Type\Types\MixedType;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
use WeakMap;
|
||||
|
||||
/** @internal */
|
||||
final class UnionFormatter implements TypeFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private UnionType $type,
|
||||
) {}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::this()->callMethod(
|
||||
method: $this->methodName(),
|
||||
arguments: [
|
||||
$valueNode,
|
||||
Node::variable('references'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
$methodName = $this->methodName();
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
$defaultDefinition = $definitionBuilder->for(MixedType::get());
|
||||
|
||||
$class = $defaultDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
$nodes = [];
|
||||
|
||||
foreach ($this->type->types() as $subType) {
|
||||
$definition = $definitionBuilder->for($subType)->markAsSure();
|
||||
|
||||
$class = $definition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
$nodes[] = Node::if(
|
||||
condition: new TypeAcceptNode(Node::variable('value'), $definition->type),
|
||||
body: Node::return(
|
||||
$definition->typeFormatter()->formatValueNode(Node::variable('value')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$nodes[] = Node::return(
|
||||
$defaultDefinition->typeFormatter()->formatValueNode(Node::variable('value')),
|
||||
);
|
||||
|
||||
return $class->withMethods(
|
||||
Node::method($methodName)
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', 'mixed'),
|
||||
Node::parameterDeclaration('references', WeakMap::class),
|
||||
)
|
||||
->withReturnType('mixed')
|
||||
->withBody(...$nodes),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function methodName(): string
|
||||
{
|
||||
return 'transform_union_' . hash('crc32', $this->type->toString());
|
||||
}
|
||||
}
|
||||
29
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/UnitEnumFormatter.php
vendored
Normal file
29
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/UnitEnumFormatter.php
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use BackedEnum;
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
|
||||
/** @internal */
|
||||
final class UnitEnumFormatter implements TypeFormatter
|
||||
{
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::ternary(
|
||||
condition: $valueNode->instanceOf(BackedEnum::class),
|
||||
ifTrue: $valueNode->access('value'),
|
||||
ifFalse: $valueNode->access('name'),
|
||||
);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
78
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/UnsureTypeFormatter.php
vendored
Normal file
78
vendor/cuyz/valinor/src/Normalizer/Transformer/Compiler/TypeFormatter/UnsureTypeFormatter.php
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer\Compiler\TypeFormatter;
|
||||
|
||||
use CuyZ\Valinor\Compiler\Library\TypeAcceptNode;
|
||||
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
|
||||
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
|
||||
use CuyZ\Valinor\Compiler\Node;
|
||||
use CuyZ\Valinor\Normalizer\Transformer\Compiler\TransformerDefinitionBuilder;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\MixedType;
|
||||
|
||||
/** @internal */
|
||||
final class UnsureTypeFormatter implements TypeFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private TypeFormatter $delegate,
|
||||
private Type $unsureType,
|
||||
) {}
|
||||
|
||||
public function formatValueNode(ComplianceNode $valueNode): Node
|
||||
{
|
||||
return Node::this()->callMethod(
|
||||
method: $this->methodName(),
|
||||
arguments: [
|
||||
$valueNode,
|
||||
Node::variable('references'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode
|
||||
{
|
||||
$methodName = $this->methodName();
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
$class = $this->delegate->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
$defaultDefinition = $definitionBuilder->for(MixedType::get());
|
||||
|
||||
$class = $defaultDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder);
|
||||
|
||||
return $class->withMethods(
|
||||
Node::method($methodName)
|
||||
->witParameters(
|
||||
Node::parameterDeclaration('value', 'mixed'),
|
||||
Node::parameterDeclaration('references', \WeakMap::class),
|
||||
)
|
||||
->withReturnType('mixed')
|
||||
->withBody(
|
||||
Node::if(
|
||||
condition: Node::negate(
|
||||
(new TypeAcceptNode(Node::variable('value'), $this->unsureType->nativeType()))->wrap(),
|
||||
),
|
||||
body: Node::return($defaultDefinition->typeFormatter()->formatValueNode(Node::variable('value'))),
|
||||
),
|
||||
Node::return(
|
||||
$this->delegate->formatValueNode(Node::variable('value')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function methodName(): string
|
||||
{
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($this->unsureType->toString()));
|
||||
|
||||
return "transform_unsure_{$slug}_" . hash('crc32', $this->unsureType->toString());
|
||||
}
|
||||
}
|
||||
13
vendor/cuyz/valinor/src/Normalizer/Transformer/EmptyObject.php
vendored
Normal file
13
vendor/cuyz/valinor/src/Normalizer/Transformer/EmptyObject.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer;
|
||||
|
||||
use CuyZ\Valinor\Utility\IsSingleton;
|
||||
|
||||
/** @internal */
|
||||
final class EmptyObject
|
||||
{
|
||||
use IsSingleton;
|
||||
}
|
||||
195
vendor/cuyz/valinor/src/Normalizer/Transformer/RecursiveTransformer.php
vendored
Normal file
195
vendor/cuyz/valinor/src/Normalizer/Transformer/RecursiveTransformer.php
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer;
|
||||
|
||||
use BackedEnum;
|
||||
use Closure;
|
||||
use CuyZ\Valinor\Definition\AttributeDefinition;
|
||||
use CuyZ\Valinor\Definition\Attributes;
|
||||
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
|
||||
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
|
||||
use CuyZ\Valinor\Normalizer\Exception\CircularReferenceFoundDuringNormalization;
|
||||
use CuyZ\Valinor\Normalizer\Exception\TypeUnhandledByNormalizer;
|
||||
use CuyZ\Valinor\Type\Types\EnumType;
|
||||
use CuyZ\Valinor\Type\Types\NativeClassType;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use stdClass;
|
||||
use UnitEnum;
|
||||
use WeakMap;
|
||||
|
||||
use function array_map;
|
||||
use function get_object_vars;
|
||||
use function is_array;
|
||||
use function is_iterable;
|
||||
use function is_object;
|
||||
|
||||
/** @internal */
|
||||
final class RecursiveTransformer implements Transformer
|
||||
{
|
||||
public function __construct(
|
||||
private ClassDefinitionRepository $classDefinitionRepository,
|
||||
private FunctionDefinitionRepository $functionDefinitionRepository,
|
||||
private TransformerContainer $transformerContainer,
|
||||
) {}
|
||||
|
||||
public function transform(mixed $value): mixed
|
||||
{
|
||||
return $this->doTransform($value, new WeakMap()); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WeakMap<object, object> $references
|
||||
* @param list<AttributeDefinition> $attributes
|
||||
*/
|
||||
private function doTransform(mixed $value, WeakMap $references, array $attributes = []): mixed
|
||||
{
|
||||
if (is_object($value)) {
|
||||
if (isset($references[$value])) {
|
||||
throw new CircularReferenceFoundDuringNormalization($value);
|
||||
}
|
||||
|
||||
$references = clone $references;
|
||||
$references[$value] = $value;
|
||||
|
||||
$type = $value instanceof UnitEnum
|
||||
? EnumType::native($value::class)
|
||||
: new NativeClassType($value::class);
|
||||
|
||||
$classAttributes = $this->classDefinitionRepository->for($type)->attributes->toArray();
|
||||
|
||||
$attributes = [...$attributes, ...$classAttributes];
|
||||
}
|
||||
|
||||
if (! $this->transformerContainer->hasTransformers() && $attributes === []) {
|
||||
return $this->defaultTransformer($value, $references);
|
||||
}
|
||||
|
||||
if ($attributes !== []) {
|
||||
$attributes = (new Attributes(...$attributes))
|
||||
->filter(TransformerContainer::filterTransformerAttributes(...))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$transformers = [
|
||||
// First chunk of transformers to be used: attributes, coming from
|
||||
// class or property.
|
||||
...array_map(
|
||||
fn (AttributeDefinition $attribute) => $attribute->instantiate()->normalize(...), // @phpstan-ignore-line / We know the method exists
|
||||
$attributes,
|
||||
),
|
||||
// Second chunk of transformers to be used: registered transformers.
|
||||
...$this->transformerContainer->transformers(),
|
||||
// Last one: default transformer.
|
||||
fn (mixed $value) => $this->defaultTransformer($value, $references),
|
||||
];
|
||||
|
||||
return call_user_func($this->nextTransformer($transformers, $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-list<callable> $transformers
|
||||
*/
|
||||
private function nextTransformer(array $transformers, mixed $value): callable
|
||||
{
|
||||
$transformer = array_shift($transformers);
|
||||
|
||||
if ($transformers === []) {
|
||||
return fn () => $transformer($value);
|
||||
}
|
||||
|
||||
$function = $this->functionDefinitionRepository->for($transformer);
|
||||
|
||||
if (! $function->parameters->at(0)->type->accepts($value)) {
|
||||
return $this->nextTransformer($transformers, $value);
|
||||
}
|
||||
|
||||
return fn () => $transformer($value, fn () => call_user_func($this->nextTransformer($transformers, $value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WeakMap<object, object> $references
|
||||
* @return iterable<mixed>|scalar|null
|
||||
*/
|
||||
private function defaultTransformer(mixed $value, WeakMap $references): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_iterable($value)) {
|
||||
if (is_array($value)) {
|
||||
return array_map(
|
||||
fn (mixed $value) => $this->doTransform($value, $references),
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
return (function () use ($value, $references) {
|
||||
foreach ($value as $key => $item) {
|
||||
yield $key => $this->doTransform($item, $references);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
if (is_object($value) && ! $value instanceof Closure) {
|
||||
if ($value instanceof UnitEnum) {
|
||||
return $value instanceof BackedEnum ? $value->value : $value->name;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value->format('Y-m-d\\TH:i:s.uP'); // RFC 3339
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeZone) {
|
||||
return $value->getName();
|
||||
}
|
||||
|
||||
if ($value::class === stdClass::class) {
|
||||
$result = (array)$value;
|
||||
|
||||
if ($result === []) {
|
||||
return EmptyObject::get();
|
||||
}
|
||||
|
||||
return array_map(
|
||||
fn (mixed $value) => $this->doTransform($value, $references),
|
||||
$result,
|
||||
);
|
||||
}
|
||||
|
||||
$values = (fn () => get_object_vars($this))->call($value);
|
||||
|
||||
$transformed = [];
|
||||
|
||||
$class = $this->classDefinitionRepository->for(new NativeClassType($value::class));
|
||||
|
||||
foreach ($values as $key => $subValue) {
|
||||
$property = $class->properties->get($key);
|
||||
|
||||
$keyTransformersAttributes = $property->attributes->filter(TransformerContainer::filterKeyTransformerAttributes(...));
|
||||
|
||||
foreach ($keyTransformersAttributes as $attribute) {
|
||||
$method = $attribute->class->methods->get('normalizeKey');
|
||||
|
||||
if ($method->parameters->count() === 0 || $method->parameters->at(0)->type->accepts($key)) {
|
||||
/** @var string $key */
|
||||
$key = $attribute->instantiate()->normalizeKey($key); // @phpstan-ignore-line / We know the method exists
|
||||
}
|
||||
}
|
||||
|
||||
$transformed[$key] = $this->doTransform($subValue, $references, $property->attributes->toArray());
|
||||
}
|
||||
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
throw new TypeUnhandledByNormalizer($value);
|
||||
}
|
||||
}
|
||||
11
vendor/cuyz/valinor/src/Normalizer/Transformer/Transformer.php
vendored
Normal file
11
vendor/cuyz/valinor/src/Normalizer/Transformer/Transformer.php
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer;
|
||||
|
||||
/** @internal */
|
||||
interface Transformer
|
||||
{
|
||||
public function transform(mixed $value): mixed;
|
||||
}
|
||||
102
vendor/cuyz/valinor/src/Normalizer/Transformer/TransformerContainer.php
vendored
Normal file
102
vendor/cuyz/valinor/src/Normalizer/Transformer/TransformerContainer.php
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Normalizer\Transformer;
|
||||
|
||||
use CuyZ\Valinor\Definition\AttributeDefinition;
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
|
||||
use CuyZ\Valinor\Normalizer\Exception\KeyTransformerHasTooManyParameters;
|
||||
use CuyZ\Valinor\Normalizer\Exception\KeyTransformerParameterInvalidType;
|
||||
use CuyZ\Valinor\Normalizer\Exception\TransformerHasInvalidCallableParameter;
|
||||
use CuyZ\Valinor\Normalizer\Exception\TransformerHasNoParameter;
|
||||
use CuyZ\Valinor\Normalizer\Exception\TransformerHasTooManyParameters;
|
||||
use CuyZ\Valinor\Type\StringType;
|
||||
use CuyZ\Valinor\Type\Types\CallableType;
|
||||
|
||||
/** @internal */
|
||||
final class TransformerContainer
|
||||
{
|
||||
private bool $transformersCallablesWereChecked = false;
|
||||
|
||||
public function __construct(
|
||||
private FunctionDefinitionRepository $functionDefinitionRepository,
|
||||
/** @var list<callable> */
|
||||
private array $transformers,
|
||||
) {}
|
||||
|
||||
public function hasTransformers(): bool
|
||||
{
|
||||
return $this->transformers !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<callable>
|
||||
*/
|
||||
public function transformers(): array
|
||||
{
|
||||
if (! $this->transformersCallablesWereChecked) {
|
||||
$this->transformersCallablesWereChecked = true;
|
||||
|
||||
foreach ($this->transformers as $transformer) {
|
||||
$function = $this->functionDefinitionRepository->for($transformer);
|
||||
|
||||
self::checkTransformer($function);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->transformers;
|
||||
}
|
||||
|
||||
public static function filterTransformerAttributes(AttributeDefinition $attribute): bool
|
||||
{
|
||||
return $attribute->class->methods->has('normalize')
|
||||
&& self::checkTransformer($attribute->class->methods->get('normalize'));
|
||||
}
|
||||
|
||||
public static function filterKeyTransformerAttributes(AttributeDefinition $attribute): bool
|
||||
{
|
||||
return $attribute->class->methods->has('normalizeKey')
|
||||
&& self::checkKeyTransformer($attribute->class->methods->get('normalizeKey'));
|
||||
}
|
||||
|
||||
private static function checkTransformer(MethodDefinition|FunctionDefinition $method): bool
|
||||
{
|
||||
$parameters = $method->parameters;
|
||||
|
||||
if ($parameters->count() === 0) {
|
||||
throw new TransformerHasNoParameter($method);
|
||||
}
|
||||
|
||||
if ($parameters->count() > 2) {
|
||||
throw new TransformerHasTooManyParameters($method);
|
||||
}
|
||||
|
||||
if ($parameters->count() > 1 && ! $parameters->at(1)->nativeType instanceof CallableType) {
|
||||
throw new TransformerHasInvalidCallableParameter($method, $parameters->at(1)->nativeType);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function checkKeyTransformer(MethodDefinition $method): bool
|
||||
{
|
||||
$parameters = $method->parameters;
|
||||
|
||||
if ($parameters->count() > 1) {
|
||||
throw new KeyTransformerHasTooManyParameters($method);
|
||||
}
|
||||
|
||||
if ($parameters->count() > 0) {
|
||||
$type = $parameters->at(0)->type;
|
||||
|
||||
if (! $type instanceof StringType) {
|
||||
throw new KeyTransformerParameterInvalidType($method);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user