mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
feat: introduce automatic union of objects inferring during mapping
When the mapper needs to map a source to a union of objects, it will try to guess which object it will map to, based on the needed arguments of the objects, and the values contained in the source. ```php final class UnionOfObjects { public readonly SomeFooObject|SomeBarObject $object; } final class SomeFooObject { public readonly string $foo; } final class SomeBarObject { public readonly string $bar; } // Will map to an instance of `SomeFooObject` (new \CuyZ\Valinor\MapperBuilder()) ->mapper() ->map(UnionOfObjects::class, ['foo' => 'foo']); // Will map to an instance of `SomeBarObject` (new \CuyZ\Valinor\MapperBuilder()) ->mapper() ->map(UnionOfObjects::class, ['bar' => 'bar']); ```
This commit is contained in:
parent
8a74147d4c
commit
79d7c266ec
@ -21,7 +21,6 @@ use CuyZ\Valinor\Mapper\Object\Factory\AttributeObjectBuilderFactory;
|
||||
use CuyZ\Valinor\Mapper\Object\Factory\BasicObjectBuilderFactory;
|
||||
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ArrayNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\CasterNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ClassNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\EnumNodeBuilder;
|
||||
@ -31,12 +30,13 @@ use CuyZ\Valinor\Mapper\Tree\Builder\NodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ScalarNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ShapedArrayNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Visitor\AggregateShellVisitor;
|
||||
use CuyZ\Valinor\Mapper\Tree\Visitor\AttributeShellVisitor;
|
||||
use CuyZ\Valinor\Mapper\Tree\Visitor\ObjectBindingShellVisitor;
|
||||
use CuyZ\Valinor\Mapper\Tree\Visitor\InterfaceShellVisitor;
|
||||
use CuyZ\Valinor\Mapper\Tree\Visitor\ObjectBindingShellVisitor;
|
||||
use CuyZ\Valinor\Mapper\Tree\Visitor\ShellVisitor;
|
||||
use CuyZ\Valinor\Mapper\Tree\Visitor\UnionShellVisitor;
|
||||
use CuyZ\Valinor\Mapper\TreeMapper;
|
||||
@ -49,6 +49,7 @@ use CuyZ\Valinor\Type\Parser\Template\BasicTemplateParser;
|
||||
use CuyZ\Valinor\Type\Parser\Template\TemplateParser;
|
||||
use CuyZ\Valinor\Type\Parser\TypeParser;
|
||||
use CuyZ\Valinor\Type\Resolver\Union\UnionNullNarrower;
|
||||
use CuyZ\Valinor\Type\Resolver\Union\UnionObjectNarrower;
|
||||
use CuyZ\Valinor\Type\Resolver\Union\UnionScalarNarrower;
|
||||
use CuyZ\Valinor\Type\ScalarType;
|
||||
use CuyZ\Valinor\Type\Types\ArrayType;
|
||||
@ -91,7 +92,13 @@ final class Container
|
||||
ShellVisitor::class => function () use ($settings): ShellVisitor {
|
||||
return new AggregateShellVisitor(
|
||||
new UnionShellVisitor(
|
||||
new UnionNullNarrower(new UnionScalarNarrower())
|
||||
new UnionNullNarrower(
|
||||
new UnionObjectNarrower(
|
||||
new UnionScalarNarrower(),
|
||||
$this->get(ClassDefinitionRepository::class),
|
||||
$this->get(ObjectBuilderFactory::class),
|
||||
)
|
||||
)
|
||||
),
|
||||
new InterfaceShellVisitor(
|
||||
$settings->interfaceMapping,
|
||||
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Type\Resolver\Exception;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\Message;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
use RuntimeException;
|
||||
|
||||
final class CannotResolveObjectTypeFromUnion extends RuntimeException implements Message
|
||||
{
|
||||
public function __construct(UnionType $unionType)
|
||||
{
|
||||
parent::__construct(
|
||||
"Impossible to resolve the object type from the union `$unionType`.",
|
||||
1641406600
|
||||
);
|
||||
}
|
||||
}
|
116
src/Type/Resolver/Union/UnionObjectNarrower.php
Normal file
116
src/Type/Resolver/Union/UnionObjectNarrower.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Type\Resolver\Union;
|
||||
|
||||
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
|
||||
use CuyZ\Valinor\Mapper\Object\Argument;
|
||||
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
use CuyZ\Valinor\Type\Resolver\Exception\CannotResolveObjectTypeFromUnion;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
|
||||
use function array_map;
|
||||
use function array_pop;
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function ksort;
|
||||
|
||||
final class UnionObjectNarrower implements UnionNarrower
|
||||
{
|
||||
private UnionNarrower $delegate;
|
||||
|
||||
private ClassDefinitionRepository $classDefinitionRepository;
|
||||
|
||||
private ObjectBuilderFactory $objectBuilderFactory;
|
||||
|
||||
public function __construct(
|
||||
UnionNarrower $delegate,
|
||||
ClassDefinitionRepository $classDefinitionRepository,
|
||||
ObjectBuilderFactory $objectBuilderFactory
|
||||
) {
|
||||
$this->delegate = $delegate;
|
||||
$this->classDefinitionRepository = $classDefinitionRepository;
|
||||
$this->objectBuilderFactory = $objectBuilderFactory;
|
||||
}
|
||||
|
||||
public function narrow(UnionType $unionType, $source): Type
|
||||
{
|
||||
if (! is_array($source)) {
|
||||
return $this->delegate->narrow($unionType, $source);
|
||||
}
|
||||
|
||||
$isIncremental = true;
|
||||
$types = [];
|
||||
$argumentsList = [];
|
||||
|
||||
foreach ($unionType->types() as $type) {
|
||||
if (! $type instanceof ObjectType) {
|
||||
return $this->delegate->narrow($unionType, $source);
|
||||
}
|
||||
|
||||
$class = $this->classDefinitionRepository->for($type->signature());
|
||||
$objectBuilder = $this->objectBuilderFactory->for($class);
|
||||
$arguments = [...$objectBuilder->describeArguments()];
|
||||
|
||||
foreach ($arguments as $argument) {
|
||||
if (! isset($source[$argument->name()]) && $argument->isRequired()) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$count = count($arguments);
|
||||
|
||||
if (isset($types[$count])) {
|
||||
$isIncremental = false;
|
||||
/** @infection-ignore-all */
|
||||
break;
|
||||
}
|
||||
|
||||
$types[$count] = $type;
|
||||
$argumentsList[$count] = $arguments;
|
||||
}
|
||||
|
||||
ksort($types);
|
||||
ksort($argumentsList);
|
||||
|
||||
if ($isIncremental && count($types) >= 1 && $this->argumentsAreSharedAcrossList($argumentsList)) {
|
||||
return array_pop($types);
|
||||
}
|
||||
|
||||
throw new CannotResolveObjectTypeFromUnion($unionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<Argument>> $argumentsList
|
||||
*/
|
||||
private function argumentsAreSharedAcrossList(array $argumentsList): bool
|
||||
{
|
||||
$namesList = [];
|
||||
|
||||
foreach ($argumentsList as $arguments) {
|
||||
$namesList[] = array_map(fn (Argument $argument) => $argument->name(), $arguments);
|
||||
}
|
||||
|
||||
while ($current = array_shift($namesList)) {
|
||||
if (count($namesList) === 0) {
|
||||
/** @infection-ignore-all */
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($current as $name) {
|
||||
foreach ($namesList as $other) {
|
||||
if (! in_array($name, $other, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
21
tests/Integration/Mapping/Fixture/NativeUnionOfObjects.php
Normal file
21
tests/Integration/Mapping/Fixture/NativeUnionOfObjects.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Integration\Mapping\Fixture;
|
||||
|
||||
// @PHP8.0 move inside \CuyZ\Valinor\Tests\Integration\Mapping\Object\UnionOfObjectsMappingTest
|
||||
final class NativeUnionOfObjects
|
||||
{
|
||||
public SomeFooObject|SomeBarObject $object;
|
||||
}
|
||||
|
||||
final class SomeFooObject
|
||||
{
|
||||
public string $foo;
|
||||
}
|
||||
|
||||
final class SomeBarObject
|
||||
{
|
||||
public string $bar;
|
||||
}
|
151
tests/Integration/Mapping/Object/UnionOfObjectsMappingTest.php
Normal file
151
tests/Integration/Mapping/Object/UnionOfObjectsMappingTest.php
Normal file
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Integration\Mapping\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\MappingError;
|
||||
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
||||
use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\NativeUnionOfObjects;
|
||||
|
||||
final class UnionOfObjectsMappingTest extends IntegrationTest
|
||||
{
|
||||
/**
|
||||
* @requires PHP >= 8
|
||||
*/
|
||||
public function test_object_type_is_narrowed_correctly_for_simple_case(): void
|
||||
{
|
||||
try {
|
||||
$resultFoo = $this->mapperBuilder->mapper()->map(NativeUnionOfObjects::class, [
|
||||
'foo' => 'foo'
|
||||
]);
|
||||
$resultBar = $this->mapperBuilder->mapper()->map(NativeUnionOfObjects::class, [
|
||||
'bar' => 'bar'
|
||||
]);
|
||||
} catch (MappingError $error) {
|
||||
$this->mappingFail($error);
|
||||
}
|
||||
|
||||
self::assertInstanceOf(\CuyZ\Valinor\Tests\Integration\Mapping\Fixture\SomeFooObject::class, $resultFoo->object);
|
||||
self::assertInstanceOf(\CuyZ\Valinor\Tests\Integration\Mapping\Fixture\SomeBarObject::class, $resultBar->object);
|
||||
}
|
||||
|
||||
public function test_object_type_is_narrowed_correctly_for_simple_array_case(): void
|
||||
{
|
||||
try {
|
||||
$result = $this->mapperBuilder->mapper()->map(UnionOfFooAndBar::class, [
|
||||
'foo' => ['foo' => 'foo'],
|
||||
'bar' => ['bar' => 'bar'],
|
||||
]);
|
||||
} catch (MappingError $error) {
|
||||
$this->mappingFail($error);
|
||||
}
|
||||
|
||||
self::assertInstanceOf(SomeFooObject::class, $result->objects['foo']);
|
||||
self::assertInstanceOf(SomeBarObject::class, $result->objects['bar']);
|
||||
}
|
||||
|
||||
public function test_objects_sharing_one_property_are_resolved_correctly(): void
|
||||
{
|
||||
try {
|
||||
$result = $this->mapperBuilder->mapper()->map(UnionOfFooAndBarAndFoo::class, [
|
||||
['foo' => 'foo'],
|
||||
['foo' => 'foo', 'bar' => 'bar'],
|
||||
]);
|
||||
} catch (MappingError $error) {
|
||||
$this->mappingFail($error);
|
||||
}
|
||||
|
||||
self::assertInstanceOf(SomeFooObject::class, $result->objects[0]);
|
||||
self::assertInstanceOf(SomeFooAndBarObject::class, $result->objects[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @dataProvider mapping_error_when_cannot_resolve_union_data_provider
|
||||
*
|
||||
* @param class-string $className
|
||||
* @param mixed[] $source
|
||||
*/
|
||||
public function test_mapping_error_when_cannot_resolve_union(string $className, array $source): void
|
||||
{
|
||||
try {
|
||||
$this->mapperBuilder->mapper()->map($className, $source);
|
||||
|
||||
self::fail('No mapping error when one was expected');
|
||||
} catch (MappingError $exception) {
|
||||
$error = $exception->node()->children()['objects']->children()[0]->messages()[0];
|
||||
|
||||
self::assertSame('1641406600', $error->code());
|
||||
}
|
||||
}
|
||||
|
||||
public function mapping_error_when_cannot_resolve_union_data_provider(): iterable
|
||||
{
|
||||
yield [
|
||||
'className' => UnionOfFooAndBar::class,
|
||||
'source' => [['foo' => 'foo', 'bar' => 'bar']],
|
||||
];
|
||||
yield [
|
||||
'className' => UnionOfFooAndBarAndFiz::class,
|
||||
'source' => [['foo' => 'foo', 'bar' => 'bar', 'fiz' => 'fiz']],
|
||||
];
|
||||
yield [
|
||||
'className' => UnionOfFooAndAnotherFoo::class,
|
||||
'source' => [['foo' => 'foo']],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
final class UnionOfFooAndBar
|
||||
{
|
||||
/** @var array<SomeFooObject|SomeBarObject> */
|
||||
public array $objects;
|
||||
}
|
||||
|
||||
final class UnionOfFooAndAnotherFoo
|
||||
{
|
||||
/** @var array<SomeFooObject|SomeOtherFooObject> */
|
||||
public array $objects;
|
||||
}
|
||||
|
||||
final class UnionOfFooAndBarAndFoo
|
||||
{
|
||||
/** @var array<SomeFooAndBarObject|SomeFooObject> */
|
||||
public array $objects;
|
||||
}
|
||||
|
||||
final class UnionOfFooAndBarAndFiz
|
||||
{
|
||||
/** @var array<SomeFooObject|SomeBarAndFizObject> */
|
||||
public array $objects;
|
||||
}
|
||||
|
||||
final class SomeFooObject
|
||||
{
|
||||
public string $foo;
|
||||
}
|
||||
|
||||
final class SomeOtherFooObject
|
||||
{
|
||||
public string $foo;
|
||||
}
|
||||
|
||||
final class SomeBarObject
|
||||
{
|
||||
public string $bar;
|
||||
}
|
||||
|
||||
final class SomeFooAndBarObject
|
||||
{
|
||||
public string $foo;
|
||||
|
||||
public string $bar;
|
||||
}
|
||||
|
||||
final class SomeBarAndFizObject
|
||||
{
|
||||
public string $bar;
|
||||
|
||||
public string $fiz;
|
||||
}
|
Loading…
Reference in New Issue
Block a user