mirror of
https://github.com/danog/Valinor.git
synced 2024-11-26 20:24:40 +01:00
misc!: allow object builder to yield arguments without source
The `Argument` class must now be instantiated with one of the `required` or `optional` static constructors.
This commit is contained in:
parent
e834cdc5d3
commit
8a74147d4c
@ -15,19 +15,41 @@ final class Argument
|
||||
private Type $type;
|
||||
|
||||
/** @var mixed */
|
||||
private $value;
|
||||
private $defaultValue;
|
||||
|
||||
private ?Attributes $attributes;
|
||||
private bool $isRequired = true;
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct(string $name, Type $type, $value, Attributes $attributes = null)
|
||||
private Attributes $attributes;
|
||||
|
||||
private function __construct(string $name, Type $type)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
public static function required(string $name, Type $type): self
|
||||
{
|
||||
return new self($name, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $defaultValue
|
||||
*/
|
||||
public static function optional(string $name, Type $type, $defaultValue): self
|
||||
{
|
||||
$instance = new self($name, $type);
|
||||
$instance->defaultValue = $defaultValue;
|
||||
$instance->isRequired = false;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function withAttributes(Attributes $attributes): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->attributes = $attributes;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
@ -43,9 +65,14 @@ final class Argument
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function value()
|
||||
public function defaultValue()
|
||||
{
|
||||
return $this->value;
|
||||
return $this->defaultValue;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->isRequired;
|
||||
}
|
||||
|
||||
public function attributes(): Attributes
|
||||
|
@ -5,9 +5,13 @@ 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;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayElement;
|
||||
use CuyZ\Valinor\Type\Types\ShapedArrayType;
|
||||
use CuyZ\Valinor\Type\Types\StringValueType;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
@ -24,6 +28,8 @@ 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;
|
||||
|
||||
@ -35,24 +41,35 @@ final class DateTimeObjectBuilder implements ObjectBuilder
|
||||
$this->className = $className;
|
||||
}
|
||||
|
||||
public function describeArguments($source): iterable
|
||||
public function describeArguments(): iterable
|
||||
{
|
||||
$datetime = $source;
|
||||
$format = null;
|
||||
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
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if (is_array($source)) {
|
||||
$datetime = $source['datetime'] ?? null;
|
||||
$format = $source['format'] ?? null;
|
||||
}
|
||||
|
||||
yield new Argument('datetime', new UnionType(PositiveIntegerType::get(), NonEmptyStringType::get()), $datetime);
|
||||
yield new Argument('format', new UnionType(NullType::get(), NonEmptyStringType::get()), $format);
|
||||
yield Argument::required('value', self::$argumentType);
|
||||
}
|
||||
|
||||
public function build(array $arguments): DateTimeInterface
|
||||
{
|
||||
$datetime = $arguments['datetime'];
|
||||
$format = $arguments['format'];
|
||||
$datetime = $arguments['value'];
|
||||
$format = null;
|
||||
|
||||
if (is_array($datetime)) {
|
||||
$format = $datetime['format'];
|
||||
$datetime = $datetime['datetime'];
|
||||
}
|
||||
|
||||
assert(is_string($datetime) || is_int($datetime));
|
||||
assert(is_string($format) || is_null($format));
|
||||
|
@ -10,21 +10,14 @@ use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotPublic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodClassReturnType;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodReturnType;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidSourceForObject;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MissingMethodArgument;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
|
||||
use CuyZ\Valinor\Type\Types\ClassType;
|
||||
use Exception;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function is_a;
|
||||
use function is_array;
|
||||
use function is_iterable;
|
||||
use function iterator_to_array;
|
||||
|
||||
final class MethodObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
@ -66,17 +59,14 @@ final class MethodObjectBuilder implements ObjectBuilder
|
||||
}
|
||||
}
|
||||
|
||||
public function describeArguments($source): iterable
|
||||
public function describeArguments(): iterable
|
||||
{
|
||||
$source = $this->transformSource($source);
|
||||
|
||||
foreach ($this->method->parameters() as $parameter) {
|
||||
$name = $parameter->name();
|
||||
$type = $parameter->type();
|
||||
$attributes = $parameter->attributes();
|
||||
$value = array_key_exists($name, $source) ? $source[$name] : $parameter->defaultValue();
|
||||
$argument = $parameter->isOptional()
|
||||
? Argument::optional($parameter->name(), $parameter->type(), $parameter->defaultValue())
|
||||
: Argument::required($parameter->name(), $parameter->type());
|
||||
|
||||
yield new Argument($name, $type, $value, $attributes);
|
||||
yield $argument->withAttributes($parameter->attributes());
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,48 +82,18 @@ final class MethodObjectBuilder implements ObjectBuilder
|
||||
$methodName = $this->method->name();
|
||||
|
||||
try {
|
||||
// @PHP8.0 `array_values` can be removed
|
||||
$arguments = array_values($arguments);
|
||||
|
||||
if (! $this->method->isStatic()) {
|
||||
// @PHP8.0 `array_values` can be removed
|
||||
/** @infection-ignore-all */
|
||||
return new $className(...array_values($arguments));
|
||||
return new $className(...$arguments);
|
||||
}
|
||||
|
||||
// @PHP8.0 `array_values` can be removed
|
||||
/** @infection-ignore-all */
|
||||
return $className::$methodName(...array_values($arguments)); // @phpstan-ignore-line
|
||||
return $className::$methodName(...$arguments); // @phpstan-ignore-line
|
||||
} catch (Exception $exception) {
|
||||
throw ThrowableMessage::from($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $source
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function transformSource($source): array
|
||||
{
|
||||
if ($source === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_iterable($source) && ! is_array($source)) {
|
||||
$source = iterator_to_array($source);
|
||||
}
|
||||
|
||||
$parameters = $this->method->parameters();
|
||||
|
||||
if (count($parameters) === 1) {
|
||||
$name = array_keys(iterator_to_array($parameters))[0];
|
||||
|
||||
if (! is_array($source) || ! array_key_exists($name, $source)) {
|
||||
$source = [$name => $source];
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_array($source)) {
|
||||
throw new InvalidSourceForObject($source);
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,9 @@ namespace CuyZ\Valinor\Mapper\Object;
|
||||
interface ObjectBuilder
|
||||
{
|
||||
/**
|
||||
* @param mixed $source
|
||||
* @return iterable<Argument>
|
||||
*/
|
||||
public function describeArguments($source): iterable;
|
||||
public function describeArguments(): iterable;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $arguments
|
||||
|
@ -5,14 +5,9 @@ declare(strict_types=1);
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidSourceForObject;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MissingPropertyArgument;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function iterator_to_array;
|
||||
|
||||
final class ReflectionObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
@ -23,17 +18,14 @@ final class ReflectionObjectBuilder implements ObjectBuilder
|
||||
$this->class = $class;
|
||||
}
|
||||
|
||||
public function describeArguments($source): iterable
|
||||
public function describeArguments(): iterable
|
||||
{
|
||||
$source = $this->transformSource($source);
|
||||
|
||||
foreach ($this->class->properties() as $property) {
|
||||
$name = $property->name();
|
||||
$type = $property->type();
|
||||
$attributes = $property->attributes();
|
||||
$value = array_key_exists($name, $source) ? $source[$name] : $property->defaultValue();
|
||||
$argument = $property->hasDefaultValue()
|
||||
? Argument::optional($property->name(), $property->type(), $property->defaultValue())
|
||||
: Argument::required($property->name(), $property->type());
|
||||
|
||||
yield new Argument($name, $type, $value, $attributes);
|
||||
yield $argument->withAttributes($property->attributes());
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,35 +49,4 @@ final class ReflectionObjectBuilder implements ObjectBuilder
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $source
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function transformSource($source): array
|
||||
{
|
||||
if ($source === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_iterable($source) && ! is_array($source)) {
|
||||
$source = iterator_to_array($source);
|
||||
}
|
||||
|
||||
$properties = $this->class->properties();
|
||||
|
||||
if (count($properties) === 1) {
|
||||
$name = array_keys(iterator_to_array($properties))[0];
|
||||
|
||||
if (! is_array($source) || ! array_key_exists($name, $source)) {
|
||||
$source = [$name => $source];
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_array($source)) {
|
||||
throw new InvalidSourceForObject($source);
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
|
@ -5,13 +5,20 @@ 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\Exception\InvalidSourceForObject;
|
||||
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
|
||||
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Node;
|
||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||
use CuyZ\Valinor\Type\Types\ClassType;
|
||||
|
||||
use function array_key_exists;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function is_iterable;
|
||||
use function iterator_to_array;
|
||||
|
||||
final class ClassNodeBuilder implements NodeBuilder
|
||||
{
|
||||
@ -28,7 +35,7 @@ final class ClassNodeBuilder implements NodeBuilder
|
||||
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
|
||||
{
|
||||
$type = $shell->type();
|
||||
$value = $shell->value();
|
||||
$source = $shell->value();
|
||||
|
||||
assert($type instanceof ClassType);
|
||||
|
||||
@ -36,10 +43,16 @@ final class ClassNodeBuilder implements NodeBuilder
|
||||
$builder = $this->objectBuilderFactory->for($class);
|
||||
|
||||
$children = [];
|
||||
$arguments = [...$builder->describeArguments()];
|
||||
$source = $this->transformSource($source, ...$arguments);
|
||||
|
||||
foreach ($builder->describeArguments($value) as $arg) {
|
||||
$child = $shell->child($arg->name(), $arg->type(), $arg->value(), $arg->attributes());
|
||||
foreach ($arguments as $argument) {
|
||||
$name = $argument->name();
|
||||
$type = $argument->type();
|
||||
$attributes = $argument->attributes();
|
||||
$value = array_key_exists($name, $source) ? $source[$name] : $argument->defaultValue();
|
||||
|
||||
$child = $shell->child($name, $type, $value, $attributes);
|
||||
$children[] = $rootBuilder->build($child);
|
||||
}
|
||||
|
||||
@ -48,6 +61,35 @@ final class ClassNodeBuilder implements NodeBuilder
|
||||
return Node::branch($shell, $object, $children);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $source
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function transformSource($source, Argument ...$arguments): array
|
||||
{
|
||||
if ($source === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_iterable($source) && ! is_array($source)) {
|
||||
$source = iterator_to_array($source);
|
||||
}
|
||||
|
||||
if (count($arguments) === 1) {
|
||||
$name = $arguments[0]->name();
|
||||
|
||||
if (! is_array($source) || ! array_key_exists($name, $source)) {
|
||||
$source = [$name => $source];
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_array($source)) {
|
||||
throw new InvalidSourceForObject($source);
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node[] $children
|
||||
*/
|
||||
|
@ -9,7 +9,7 @@ use stdClass;
|
||||
|
||||
final class FakeObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
public function describeArguments($source): iterable
|
||||
public function describeArguments(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -103,10 +103,9 @@ final class DateTimeMappingTest extends IntegrationTest
|
||||
],
|
||||
]);
|
||||
} catch (MappingError $exception) {
|
||||
$error = $exception->node()->children()['dateTime']->children()['datetime']->messages()[0];
|
||||
$error = $exception->node()->children()['dateTime']->children()['value']->messages()[0];
|
||||
|
||||
self::assertSame('1618742357', $error->code());
|
||||
self::assertSame('Cannot assign an empty value to union type `positive-int|non-empty-string`.', (string)$error);
|
||||
self::assertSame('1607027306', $error->code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ final class ObjectValuesMappingTest extends IntegrationTest
|
||||
public function test_values_are_mapped_properly(): void
|
||||
{
|
||||
$source = [
|
||||
'string' => 'foo',
|
||||
'object' => [
|
||||
'value' => 'foo',
|
||||
],
|
||||
@ -28,17 +29,36 @@ final class ObjectValuesMappingTest extends IntegrationTest
|
||||
self::assertSame('foo', $result->object->value);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_invalid_iterable_source_throws_exception(): void
|
||||
{
|
||||
$source = 'foo';
|
||||
|
||||
foreach ([ObjectValues::class, ObjectValuesWithConstructor::class] as $class) {
|
||||
try {
|
||||
$this->mapperBuilder->mapper()->map($class, $source);
|
||||
} catch (MappingError $exception) {
|
||||
$error = $exception->node()->messages()[0];
|
||||
|
||||
self::assertSame('1632903281', $error->code());
|
||||
self::assertSame('Invalid source type `string`, it must be an iterable.', (string)$error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectValues
|
||||
{
|
||||
public SimpleObject $object;
|
||||
|
||||
public string $string;
|
||||
}
|
||||
|
||||
class ObjectValuesWithConstructor extends ObjectValues
|
||||
{
|
||||
public function __construct(SimpleObject $object)
|
||||
public function __construct(SimpleObject $object, string $string)
|
||||
{
|
||||
$this->object = $object;
|
||||
$this->string = $string;
|
||||
}
|
||||
}
|
||||
|
@ -4,18 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\Argument;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotPublic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodClassReturnType;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodReturnType;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidSourceForObject;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MissingMethodArgument;
|
||||
use CuyZ\Valinor\Mapper\Object\MethodObjectBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
|
||||
use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition;
|
||||
use Generator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use RuntimeException;
|
||||
@ -118,26 +115,6 @@ final class MethodObjectBuilderTest extends TestCase
|
||||
new MethodObjectBuilder($class, 'invalidConstructor');
|
||||
}
|
||||
|
||||
public function test_invalid_source_type_throws_exception(): void
|
||||
{
|
||||
$object = new class () {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($object));
|
||||
$objectBuilder = new MethodObjectBuilder($class, '__construct');
|
||||
|
||||
$this->expectException(InvalidSourceForObject::class);
|
||||
$this->expectExceptionCode(1632903281);
|
||||
$this->expectExceptionMessage('Invalid source type `string`, it must be an iterable.');
|
||||
|
||||
/** @var Generator<Argument> $arguments */
|
||||
$arguments = $objectBuilder->describeArguments('foo');
|
||||
$arguments->current();
|
||||
}
|
||||
|
||||
public function test_missing_arguments_throws_exception(): void
|
||||
{
|
||||
$object = new class ('foo') {
|
||||
|
@ -4,12 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\Argument;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidSourceForObject;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MissingPropertyArgument;
|
||||
use CuyZ\Valinor\Mapper\Object\ReflectionObjectBuilder;
|
||||
use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition;
|
||||
use Generator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
|
||||
@ -53,26 +50,6 @@ final class ReflectionObjectBuilderTest extends TestCase
|
||||
self::assertSame('valueC', $result->valueC()); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function test_invalid_source_type_throws_exception(): void
|
||||
{
|
||||
$object = new class () {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($object));
|
||||
$objectBuilder = new ReflectionObjectBuilder($class);
|
||||
|
||||
$this->expectException(InvalidSourceForObject::class);
|
||||
$this->expectExceptionCode(1632903281);
|
||||
$this->expectExceptionMessage('Invalid source type `string`, it must be an iterable.');
|
||||
|
||||
/** @var Generator<Argument> $arguments */
|
||||
$arguments = $objectBuilder->describeArguments('foo');
|
||||
$arguments->current();
|
||||
}
|
||||
|
||||
public function test_missing_arguments_throws_exception(): void
|
||||
{
|
||||
$object = new class () {
|
||||
|
Loading…
Reference in New Issue
Block a user