mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
misc: introduce layer for object builder arguments
This commit is contained in:
parent
11e12624aa
commit
48f936275e
60
src/Mapper/Object/Arguments.php
Normal file
60
src/Mapper/Object/Arguments.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use Countable;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidArgumentIndex;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @implements IteratorAggregate<Argument>
|
||||
*/
|
||||
final class Arguments implements IteratorAggregate, Countable
|
||||
{
|
||||
/** @var Argument[] */
|
||||
private array $arguments;
|
||||
|
||||
public function __construct(Argument ...$arguments)
|
||||
{
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
public function at(int $index): Argument
|
||||
{
|
||||
if ($index >= count($this->arguments)) {
|
||||
throw new InvalidArgumentIndex($index, $this);
|
||||
}
|
||||
|
||||
return $this->arguments[$index];
|
||||
}
|
||||
|
||||
public function signature(): string
|
||||
{
|
||||
$parameters = array_map(
|
||||
fn (Argument $argument) => $argument->isRequired()
|
||||
? "{$argument->name()}: {$argument->type()}"
|
||||
: "{$argument->name()}?: {$argument->type()}",
|
||||
$this->arguments
|
||||
);
|
||||
|
||||
return 'array{' . implode(', ', $parameters) . '}';
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable<Argument>
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
yield from $this->arguments;
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\CannotParseToDateTime;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\NonEmptyStringType;
|
||||
use CuyZ\Valinor\Type\Types\NullType;
|
||||
use CuyZ\Valinor\Type\Types\PositiveIntegerType;
|
||||
@ -29,11 +28,11 @@ final class DateTimeObjectBuilder implements ObjectBuilder
|
||||
public const DATE_MYSQL = 'Y-m-d H:i:s';
|
||||
public const DATE_PGSQL = 'Y-m-d H:i:s.u';
|
||||
|
||||
private static Type $argumentType;
|
||||
|
||||
/** @var class-string<DateTime|DateTimeImmutable> */
|
||||
private string $className;
|
||||
|
||||
private Arguments $arguments;
|
||||
|
||||
/**
|
||||
* @param class-string<DateTime|DateTimeImmutable> $className
|
||||
*/
|
||||
@ -42,9 +41,10 @@ final class DateTimeObjectBuilder implements ObjectBuilder
|
||||
$this->className = $className;
|
||||
}
|
||||
|
||||
public function describeArguments(): iterable
|
||||
public function describeArguments(): Arguments
|
||||
{
|
||||
self::$argumentType ??= new UnionType(
|
||||
return $this->arguments ??= new Arguments(
|
||||
Argument::required('value', new UnionType(
|
||||
new UnionType(PositiveIntegerType::get(), NonEmptyStringType::get()),
|
||||
new ShapedArrayType(
|
||||
new ShapedArrayElement(
|
||||
@ -57,9 +57,8 @@ final class DateTimeObjectBuilder implements ObjectBuilder
|
||||
true
|
||||
),
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
yield Argument::required('value', self::$argumentType);
|
||||
}
|
||||
|
||||
public function build(array $arguments): DateTimeInterface
|
||||
|
22
src/Mapper/Object/Exception/InvalidArgumentIndex.php
Normal file
22
src/Mapper/Object/Exception/InvalidArgumentIndex.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object\Exception;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\Arguments;
|
||||
use OutOfBoundsException;
|
||||
|
||||
/** @internal */
|
||||
final class InvalidArgumentIndex extends OutOfBoundsException
|
||||
{
|
||||
public function __construct(int $index, Arguments $arguments)
|
||||
{
|
||||
$max = $arguments->count() - 1;
|
||||
|
||||
parent::__construct(
|
||||
"Index $index is out of range, it should be between 0 and $max.",
|
||||
1648672136
|
||||
);
|
||||
}
|
||||
}
|
@ -5,9 +5,14 @@ declare(strict_types=1);
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\ParameterDefinition;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
|
||||
use Exception;
|
||||
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function iterator_to_array;
|
||||
|
||||
/** @internal */
|
||||
final class FunctionObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
@ -16,6 +21,8 @@ final class FunctionObjectBuilder implements ObjectBuilder
|
||||
/** @var callable(): object */
|
||||
private $callback;
|
||||
|
||||
private Arguments $arguments;
|
||||
|
||||
/**
|
||||
* @param callable(): object $callback
|
||||
*/
|
||||
@ -25,15 +32,17 @@ final class FunctionObjectBuilder implements ObjectBuilder
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function describeArguments(): iterable
|
||||
public function describeArguments(): Arguments
|
||||
{
|
||||
foreach ($this->function->parameters() as $parameter) {
|
||||
return $this->arguments ??= new Arguments(
|
||||
...array_map(function (ParameterDefinition $parameter) {
|
||||
$argument = $parameter->isOptional()
|
||||
? Argument::optional($parameter->name(), $parameter->type(), $parameter->defaultValue())
|
||||
: Argument::required($parameter->name(), $parameter->type());
|
||||
|
||||
yield $argument->withAttributes($parameter->attributes());
|
||||
}
|
||||
return $argument->withAttributes($parameter->attributes());
|
||||
}, array_values(iterator_to_array($this->function->parameters()))) // @PHP8.1 array unpacking
|
||||
);
|
||||
}
|
||||
|
||||
public function build(array $arguments): object
|
||||
|
@ -6,6 +6,7 @@ namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use CuyZ\Valinor\Definition\ParameterDefinition;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotPublic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodClassReturnType;
|
||||
@ -13,6 +14,10 @@ use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
|
||||
use Exception;
|
||||
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function iterator_to_array;
|
||||
|
||||
/** @api */
|
||||
final class MethodObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
@ -20,6 +25,8 @@ final class MethodObjectBuilder implements ObjectBuilder
|
||||
|
||||
private MethodDefinition $method;
|
||||
|
||||
private Arguments $arguments;
|
||||
|
||||
public function __construct(ClassDefinition $class, string $methodName)
|
||||
{
|
||||
$methods = $class->methods();
|
||||
@ -48,15 +55,17 @@ final class MethodObjectBuilder implements ObjectBuilder
|
||||
}
|
||||
}
|
||||
|
||||
public function describeArguments(): iterable
|
||||
public function describeArguments(): Arguments
|
||||
{
|
||||
foreach ($this->method->parameters() as $parameter) {
|
||||
return $this->arguments ??= new Arguments(
|
||||
...array_map(function (ParameterDefinition $parameter) {
|
||||
$argument = $parameter->isOptional()
|
||||
? Argument::optional($parameter->name(), $parameter->type(), $parameter->defaultValue())
|
||||
: Argument::required($parameter->name(), $parameter->type());
|
||||
|
||||
yield $argument->withAttributes($parameter->attributes());
|
||||
}
|
||||
return $argument->withAttributes($parameter->attributes());
|
||||
}, array_values(iterator_to_array($this->method->parameters()))) // @PHP8.1 array unpacking
|
||||
);
|
||||
}
|
||||
|
||||
public function build(array $arguments): object
|
||||
|
@ -7,10 +7,7 @@ namespace CuyZ\Valinor\Mapper\Object;
|
||||
/** @internal */
|
||||
interface ObjectBuilder
|
||||
{
|
||||
/**
|
||||
* @return iterable<Argument>
|
||||
*/
|
||||
public function describeArguments(): iterable;
|
||||
public function describeArguments(): Arguments;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $arguments
|
||||
|
@ -54,7 +54,7 @@ final class ObjectBuilderFilterer
|
||||
*/
|
||||
private function filledArguments(ObjectBuilder $builder, $source)
|
||||
{
|
||||
$arguments = [...$builder->describeArguments()];
|
||||
$arguments = $builder->describeArguments();
|
||||
|
||||
if (! is_array($source)) {
|
||||
return count($arguments) === 1;
|
||||
@ -63,10 +63,10 @@ final class ObjectBuilderFilterer
|
||||
/** @infection-ignore-all */
|
||||
$filled = 0;
|
||||
|
||||
foreach ($arguments as $parameter) {
|
||||
if (isset($source[$parameter->name()])) {
|
||||
foreach ($arguments as $argument) {
|
||||
if (isset($source[$argument->name()])) {
|
||||
$filled++;
|
||||
} elseif ($parameter->isRequired()) {
|
||||
} elseif ($argument->isRequired()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -5,29 +5,37 @@ declare(strict_types=1);
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Definition\PropertyDefinition;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MissingPropertyArgument;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function iterator_to_array;
|
||||
|
||||
/** @api */
|
||||
final class ReflectionObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
private ClassDefinition $class;
|
||||
|
||||
private Arguments $arguments;
|
||||
|
||||
public function __construct(ClassDefinition $class)
|
||||
{
|
||||
$this->class = $class;
|
||||
}
|
||||
|
||||
public function describeArguments(): iterable
|
||||
public function describeArguments(): Arguments
|
||||
{
|
||||
foreach ($this->class->properties() as $property) {
|
||||
return $this->arguments ??= new Arguments(
|
||||
...array_map(function (PropertyDefinition $property) {
|
||||
$argument = $property->hasDefaultValue()
|
||||
? Argument::optional($property->name(), $property->type(), $property->defaultValue())
|
||||
: Argument::required($property->name(), $property->type());
|
||||
|
||||
yield $argument->withAttributes($property->attributes());
|
||||
}
|
||||
return $argument->withAttributes($property->attributes());
|
||||
}, array_values(iterator_to_array($this->class->properties()))) // @PHP8.1 array unpacking
|
||||
);
|
||||
}
|
||||
|
||||
public function build(array $arguments): object
|
||||
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||
|
||||
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
|
||||
use CuyZ\Valinor\Mapper\Object\Argument;
|
||||
use CuyZ\Valinor\Mapper\Object\Arguments;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidSourceForObject;
|
||||
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
|
||||
use CuyZ\Valinor\Mapper\Object\Factory\SuitableObjectBuilderNotFound;
|
||||
@ -58,9 +58,9 @@ final class ClassNodeBuilder implements NodeBuilder
|
||||
$source = $shell->value();
|
||||
|
||||
$builder = $this->builder($source, ...$classTypes);
|
||||
$arguments = [...$builder->describeArguments()];
|
||||
$arguments = $builder->describeArguments();
|
||||
|
||||
$source = $this->transformSource($source, ...$arguments);
|
||||
$source = $this->transformSource($source, $arguments);
|
||||
$children = [];
|
||||
|
||||
foreach ($arguments as $argument) {
|
||||
@ -129,7 +129,7 @@ final class ClassNodeBuilder implements NodeBuilder
|
||||
* @param mixed $source
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function transformSource($source, Argument ...$arguments): array
|
||||
private function transformSource($source, Arguments $arguments): array
|
||||
{
|
||||
if ($source === null || count($arguments) === 0) {
|
||||
return [];
|
||||
@ -140,7 +140,7 @@ final class ClassNodeBuilder implements NodeBuilder
|
||||
}
|
||||
|
||||
if (count($arguments) === 1) {
|
||||
$name = $arguments[0]->name();
|
||||
$name = $arguments->at(0)->name();
|
||||
|
||||
if (! is_array($source) || ! array_key_exists($name, $source)) {
|
||||
$source = [$name => $source];
|
||||
|
@ -4,14 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Fake\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\Arguments;
|
||||
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
||||
use stdClass;
|
||||
|
||||
final class FakeObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
public function describeArguments(): iterable
|
||||
public function describeArguments(): Arguments
|
||||
{
|
||||
return [];
|
||||
return new Arguments();
|
||||
}
|
||||
|
||||
public function build(array $arguments): object
|
||||
|
38
tests/Unit/Mapper/Object/ArgumentsTest.php
Normal file
38
tests/Unit/Mapper/Object/ArgumentsTest.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\Argument;
|
||||
use CuyZ\Valinor\Mapper\Object\Arguments;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidArgumentIndex;
|
||||
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ArgumentsTest extends TestCase
|
||||
{
|
||||
public function test_get_argument_at_invalid_index_throws_exception(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentIndex::class);
|
||||
$this->expectExceptionCode(1648672136);
|
||||
$this->expectExceptionMessage("Index 1 is out of range, it should be between 0 and 0.");
|
||||
|
||||
(new Arguments(
|
||||
Argument::required('someArgument', FakeType::permissive())
|
||||
))->at(1);
|
||||
}
|
||||
|
||||
public function test_signature_is_correct(): void
|
||||
{
|
||||
$typeA = FakeType::permissive();
|
||||
$typeB = FakeType::permissive();
|
||||
|
||||
$arguments = new Arguments(
|
||||
Argument::required('someArgumentA', $typeA),
|
||||
Argument::optional('someArgumentB', $typeB, 'defaultValue')
|
||||
);
|
||||
|
||||
self::assertSame("array{someArgumentA: $typeA, someArgumentB?: $typeB}", $arguments->signature());
|
||||
}
|
||||
}
|
22
tests/Unit/Mapper/Object/DateTimeObjectBuilderTest.php
Normal file
22
tests/Unit/Mapper/Object/DateTimeObjectBuilderTest.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\DateTimeObjectBuilder;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class DateTimeObjectBuilderTest extends TestCase
|
||||
{
|
||||
public function test_arguments_instance_stays_the_same(): void
|
||||
{
|
||||
$objectBuilder = new DateTimeObjectBuilder(DateTimeImmutable::class);
|
||||
|
||||
$argumentsA = $objectBuilder->describeArguments();
|
||||
$argumentsB = $objectBuilder->describeArguments();
|
||||
|
||||
self::assertSame($argumentsA, $argumentsB);
|
||||
}
|
||||
}
|
23
tests/Unit/Mapper/Object/FunctionObjectBuilderTest.php
Normal file
23
tests/Unit/Mapper/Object/FunctionObjectBuilderTest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
|
||||
use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use stdClass;
|
||||
|
||||
final class FunctionObjectBuilderTest extends TestCase
|
||||
{
|
||||
public function test_arguments_instance_stays_the_same(): void
|
||||
{
|
||||
$objectBuilder = new FunctionObjectBuilder(FakeFunctionDefinition::new(), fn () => new stdClass());
|
||||
|
||||
$argumentsA = $objectBuilder->describeArguments();
|
||||
$argumentsB = $objectBuilder->describeArguments();
|
||||
|
||||
self::assertSame($argumentsA, $argumentsB);
|
||||
}
|
||||
}
|
@ -151,6 +151,26 @@ final class MethodObjectBuilderTest extends TestCase
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($classWithPrivateNativeConstructor));
|
||||
new MethodObjectBuilder($class, 'someConstructor');
|
||||
}
|
||||
|
||||
public function test_arguments_instance_stays_the_same(): void
|
||||
{
|
||||
$class = new class ('foo') {
|
||||
public string $string;
|
||||
|
||||
public function __construct(string $string)
|
||||
{
|
||||
$this->string = $string;
|
||||
}
|
||||
};
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($class));
|
||||
|
||||
$objectBuilder = new MethodObjectBuilder($class, '__construct');
|
||||
|
||||
$argumentsA = $objectBuilder->describeArguments();
|
||||
$argumentsB = $objectBuilder->describeArguments();
|
||||
|
||||
self::assertSame($argumentsA, $argumentsB);
|
||||
}
|
||||
}
|
||||
|
||||
final class ObjectWithPrivateNativeConstructor
|
||||
|
@ -50,6 +50,17 @@ final class ReflectionObjectBuilderTest extends TestCase
|
||||
self::assertSame('valueC', $result->valueC()); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function test_arguments_instance_stays_the_same(): void
|
||||
{
|
||||
$class = FakeClassDefinition::new();
|
||||
$objectBuilder = new ReflectionObjectBuilder($class);
|
||||
|
||||
$argumentsA = $objectBuilder->describeArguments();
|
||||
$argumentsB = $objectBuilder->describeArguments();
|
||||
|
||||
self::assertSame($argumentsA, $argumentsB);
|
||||
}
|
||||
|
||||
public function test_missing_arguments_throws_exception(): void
|
||||
{
|
||||
$object = new class () {
|
||||
|
Loading…
Reference in New Issue
Block a user