misc: introduce layer for object builder arguments

This commit is contained in:
Romain Canon 2022-03-30 23:09:19 +02:00
parent 11e12624aa
commit 48f936275e
15 changed files with 274 additions and 55 deletions

View 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;
}
}

View File

@ -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,24 +41,24 @@ final class DateTimeObjectBuilder implements ObjectBuilder
$this->className = $className;
}
public function describeArguments(): iterable
public function describeArguments(): Arguments
{
self::$argumentType ??= new UnionType(
new UnionType(PositiveIntegerType::get(), NonEmptyStringType::get()),
new ShapedArrayType(
new ShapedArrayElement(
new StringValueType('datetime'),
new UnionType(PositiveIntegerType::get(), NonEmptyStringType::get())
),
new ShapedArrayElement(
new StringValueType('format'),
new UnionType(NullType::get(), NonEmptyStringType::get()),
true
),
)
return $this->arguments ??= new Arguments(
Argument::required('value', new UnionType(
new UnionType(PositiveIntegerType::get(), NonEmptyStringType::get()),
new ShapedArrayType(
new ShapedArrayElement(
new StringValueType('datetime'),
new UnionType(PositiveIntegerType::get(), NonEmptyStringType::get())
),
new ShapedArrayElement(
new StringValueType('format'),
new UnionType(NullType::get(), NonEmptyStringType::get()),
true
),
)
))
);
yield Argument::required('value', self::$argumentType);
}
public function build(array $arguments): DateTimeInterface

View 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
);
}
}

View File

@ -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) {
$argument = $parameter->isOptional()
? Argument::optional($parameter->name(), $parameter->type(), $parameter->defaultValue())
: Argument::required($parameter->name(), $parameter->type());
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

View File

@ -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) {
$argument = $parameter->isOptional()
? Argument::optional($parameter->name(), $parameter->type(), $parameter->defaultValue())
: Argument::required($parameter->name(), $parameter->type());
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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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) {
$argument = $property->hasDefaultValue()
? Argument::optional($property->name(), $property->type(), $property->defaultValue())
: Argument::required($property->name(), $property->type());
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

View File

@ -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];

View File

@ -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

View 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());
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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

View File

@ -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 () {