diff --git a/src/Library/Container.php b/src/Library/Container.php index ded16ad5..74b63a38 100644 --- a/src/Library/Container.php +++ b/src/Library/Container.php @@ -25,6 +25,7 @@ use CuyZ\Valinor\Mapper\Object\Factory\AttributeObjectBuilderFactory; use CuyZ\Valinor\Mapper\Object\Factory\ConstructorObjectBuilderFactory; use CuyZ\Valinor\Mapper\Object\Factory\DateTimeObjectBuilderFactory; +use CuyZ\Valinor\Mapper\Object\Factory\ObjectBindingBuilderFactory; use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory; use CuyZ\Valinor\Mapper\Object\ObjectBuilderFilterer; use CuyZ\Valinor\Mapper\Tree\Builder\ArrayNodeBuilder; @@ -45,7 +46,6 @@ use CuyZ\Valinor\Mapper\Tree\Visitor\AggregateShellVisitor; use CuyZ\Valinor\Mapper\Tree\Visitor\AttributeShellVisitor; 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\TreeMapper; use CuyZ\Valinor\Mapper\TreeMapperContainer; @@ -102,7 +102,6 @@ public function __construct(Settings $settings) $this->get(TypeParser::class), ), new AttributeShellVisitor(), - new ObjectBindingShellVisitor($settings->objectBinding), ); }, @@ -142,14 +141,21 @@ public function __construct(Settings $settings) return new ErrorCatcherNodeBuilder($builder); }, - ObjectBuilderFactory::class => function (): ObjectBuilderFactory { - return new AttributeObjectBuilderFactory( - new DateTimeObjectBuilderFactory( - new ConstructorObjectBuilderFactory( - $this->get(ObjectBuilderFilterer::class) - ) - ) + ObjectBuilderFactory::class => function () use ($settings): ObjectBuilderFactory { + $factory = new ConstructorObjectBuilderFactory( + $this->get(ObjectBuilderFilterer::class) ); + + $factory = new DateTimeObjectBuilderFactory($factory); + + $factory = new ObjectBindingBuilderFactory( + $factory, + $this->get(FunctionDefinitionRepository::class), + $this->get(ObjectBuilderFilterer::class), + $settings->objectBinding, + ); + + return new AttributeObjectBuilderFactory($factory); }, ObjectBuilderFilterer::class => fn () => new ObjectBuilderFilterer(), diff --git a/src/Library/Settings.php b/src/Library/Settings.php index 6a585cd5..bb73cfce 100644 --- a/src/Library/Settings.php +++ b/src/Library/Settings.php @@ -15,7 +15,7 @@ final class Settings /** @var array */ public array $interfaceMapping = []; - /** @var array */ + /** @var list */ public array $objectBinding = []; /** @var list */ diff --git a/src/Mapper/Object/CallbackObjectBuilder.php b/src/Mapper/Object/CallbackObjectBuilder.php new file mode 100644 index 00000000..1105111a --- /dev/null +++ b/src/Mapper/Object/CallbackObjectBuilder.php @@ -0,0 +1,47 @@ +function = $function; + $this->callback = $callback; + } + + public function describeArguments(): iterable + { + foreach ($this->function->parameters() as $parameter) { + $argument = $parameter->isOptional() + ? Argument::optional($parameter->name(), $parameter->type(), $parameter->defaultValue()) + : Argument::required($parameter->name(), $parameter->type()); + + yield $argument->withAttributes($parameter->attributes()); + } + } + + public function build(array $arguments): object + { + // @PHP8.0 `array_values` can be removed + /** @infection-ignore-all */ + $arguments = array_values($arguments); + + return ($this->callback)(...$arguments); + } +} diff --git a/src/Mapper/Object/Factory/ObjectBindingBuilderFactory.php b/src/Mapper/Object/Factory/ObjectBindingBuilderFactory.php new file mode 100644 index 00000000..567ab913 --- /dev/null +++ b/src/Mapper/Object/Factory/ObjectBindingBuilderFactory.php @@ -0,0 +1,76 @@ + */ + private array $callbacks; + + /** @var list */ + private array $functions; + + /** + * @param list $callbacks + */ + public function __construct( + ObjectBuilderFactory $delegate, + FunctionDefinitionRepository $functionDefinitionRepository, + ObjectBuilderFilterer $objectBuilderFilterer, + array $callbacks + ) { + $this->delegate = $delegate; + $this->functionDefinitionRepository = $functionDefinitionRepository; + $this->objectBuilderFilterer = $objectBuilderFilterer; + $this->callbacks = $callbacks; + } + + public function for(ClassDefinition $class, $source): ObjectBuilder + { + $builders = []; + + foreach ($this->functions() as $key => $function) { + if ($function->returnType()->matches($class->type())) { + $builders[] = new CallbackObjectBuilder($function, $this->callbacks[$key]); + } + } + + if (empty($builders)) { + return $this->delegate->for($class, $source); + } + + return $this->objectBuilderFilterer->filter($source, ...$builders); + } + + /** + * @return FunctionDefinition[] + */ + private function functions(): array + { + if (! isset($this->functions)) { + $this->functions = []; + + foreach ($this->callbacks as $key => $callback) { + $this->functions[$key] = $this->functionDefinitionRepository->for($callback); + } + } + + return $this->functions; + } +} diff --git a/src/Mapper/Tree/Builder/ClassNodeBuilder.php b/src/Mapper/Tree/Builder/ClassNodeBuilder.php index 9ee8939d..59f39358 100644 --- a/src/Mapper/Tree/Builder/ClassNodeBuilder.php +++ b/src/Mapper/Tree/Builder/ClassNodeBuilder.php @@ -122,7 +122,7 @@ private function builder($source, ClassType ...$classTypes): ObjectBuilder */ private function transformSource($source, Argument ...$arguments): array { - if ($source === null) { + if ($source === null || count($arguments) === 0) { return []; } diff --git a/src/Mapper/Tree/Visitor/ObjectBindingShellVisitor.php b/src/Mapper/Tree/Visitor/ObjectBindingShellVisitor.php deleted file mode 100644 index 78df4dc3..00000000 --- a/src/Mapper/Tree/Visitor/ObjectBindingShellVisitor.php +++ /dev/null @@ -1,36 +0,0 @@ - */ - private array $callbacks; - - /** - * @param array $callbacks - */ - public function __construct(array $callbacks) - { - $this->callbacks = $callbacks; - } - - public function visit(Shell $shell): Shell - { - $value = $shell->value(); - $signature = (string)$shell->type(); - - if (! isset($this->callbacks[$signature])) { - return $shell; - } - - $value = $this->callbacks[$signature]($value); - - return $shell->withValue($value); - } -} diff --git a/src/MapperBuilder.php b/src/MapperBuilder.php index a3e4e261..88b10d24 100644 --- a/src/MapperBuilder.php +++ b/src/MapperBuilder.php @@ -9,8 +9,6 @@ use CuyZ\Valinor\Mapper\Tree\Node; use CuyZ\Valinor\Mapper\Tree\Shell; use CuyZ\Valinor\Mapper\TreeMapper; -use CuyZ\Valinor\Utility\Reflection\Reflection; -use LogicException; /** @api */ final class MapperBuilder @@ -37,25 +35,35 @@ public function infer(string $interfaceName, callable $callback): self } /** - * @param callable(mixed): object $callback + * Defines a custom way to build an object during the mapping. + * + * The return type of the callback will be resolved by the mapping to know + * when to use it. + * + * The callback can take any arguments, that will automatically be mapped + * using the given source. These arguments can then be used to instantiate + * the object in the desired way. + * + * Example: + * + * ``` + * (new \CuyZ\Valinor\MapperBuilder()) + * ->bind(function(string $string, OtherClass $otherClass): SomeClass { + * $someClass = new SomeClass($string); + * $someClass->addOtherClass($otherClass); + * + * return $someClass; + * }) + * ->mapper() + * ->map(SomeClass::class, [ + * // … + * ]); + * ``` */ public function bind(callable $callback): self { - $reflection = Reflection::ofCallable($callback); - - $nativeType = $reflection->getReturnType(); - $typeFromDocBlock = Reflection::docBlockReturnType($reflection); - - if ($typeFromDocBlock) { - $type = $typeFromDocBlock; - } elseif ($nativeType) { - $type = Reflection::flattenType($nativeType); - } else { - throw new LogicException('No return type was found for this callable.'); - } - $clone = clone $this; - $clone->settings->objectBinding[$type] = $callback; + $clone->settings->objectBinding[] = $callback; return $clone; } diff --git a/src/Utility/Reflection/Reflection.php b/src/Utility/Reflection/Reflection.php index 93d7ccf9..d47230f9 100644 --- a/src/Utility/Reflection/Reflection.php +++ b/src/Utility/Reflection/Reflection.php @@ -173,27 +173,6 @@ public static function importedTypeAliases(ReflectionClass $reflection): array return $types; } - public static function ofCallable(callable $callable): ReflectionFunctionAbstract - { - if ($callable instanceof Closure) { - return new ReflectionFunction($callable); - } - - if (is_string($callable)) { - $parts = explode('::', $callable); - - return count($parts) > 1 - ? new ReflectionMethod($parts[0], $parts[1]) - : new ReflectionFunction($callable); - } - - if (! is_array($callable)) { - $callable = [$callable, '__invoke']; - } - - return new ReflectionMethod($callable[0], $callable[1]); - } - /** * @param ReflectionClass|ReflectionProperty|ReflectionFunctionAbstract $reflection */ diff --git a/tests/Integration/Mapping/ObjectBindingMappingTest.php b/tests/Integration/Mapping/ObjectBindingMappingTest.php index 92fb9879..0639a0e4 100644 --- a/tests/Integration/Mapping/ObjectBindingMappingTest.php +++ b/tests/Integration/Mapping/ObjectBindingMappingTest.php @@ -8,7 +8,6 @@ use CuyZ\Valinor\Tests\Integration\IntegrationTest; use DateTime; use DateTimeImmutable; -use DateTimeInterface; use stdClass; use function get_class; @@ -26,9 +25,7 @@ public function test_bind_object_binds_object(): void $result = $this->mapperBuilder ->bind(fn (): stdClass => $object) ->mapper() - ->map(get_class($class), [ - 'object' => new stdClass(), - ]); + ->map(get_class($class), []); } catch (MappingError $error) { $this->mappingFail($error); } @@ -47,9 +44,7 @@ public function test_bind_object_with_docblock_parameter_binds_object(): void $result = $this->mapperBuilder ->bind(/** @return stdClass */ fn () => $object) ->mapper() - ->map(get_class($class), [ - 'object' => new stdClass(), - ]); + ->map(get_class($class), []); } catch (MappingError $error) { $this->mappingFail($error); } @@ -68,7 +63,6 @@ public function test_bind_datetime_binds_datetime(): void ->bind(fn (): DateTimeImmutable => $defaultImmutable) ->mapper() ->map(SimpleDateTimeValues::class, [ - 'dateTimeInterface' => 1357047105, 'dateTimeImmutable' => 1357047105, 'dateTime' => 1357047105, ]); @@ -76,16 +70,58 @@ public function test_bind_datetime_binds_datetime(): void $this->mappingFail($error); } - self::assertSame($defaultImmutable, $result->dateTimeInterface); self::assertSame($defaultImmutable, $result->dateTimeImmutable); self::assertSame($default, $result->dateTime); } + + public function test_bind_object_with_one_argument_binds_object(): void + { + try { + $result = $this->mapperBuilder + ->bind(function (int $int): stdClass { + $class = new stdClass(); + $class->int = $int; + + return $class; + }) + ->mapper() + ->map(stdClass::class, 1337); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame(1337, $result->int); + } + + public function test_bind_object_with_arguments_binds_object(): void + { + try { + $result = $this->mapperBuilder + ->bind(function (string $string, int $int, float $float = 1337.404): stdClass { + $class = new stdClass(); + $class->string = $string; + $class->int = $int; + $class->float = $float; + + return $class; + }) + ->mapper() + ->map(stdClass::class, [ + 'string' => 'foo', + 'int' => 42, + ]); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $result->string); + self::assertSame(42, $result->int); + self::assertSame(1337.404, $result->float); + } } final class SimpleDateTimeValues { - public DateTimeInterface $dateTimeInterface; - public DateTimeImmutable $dateTimeImmutable; public DateTime $dateTime; diff --git a/tests/Unit/MapperBuilderTest.php b/tests/Unit/MapperBuilderTest.php index b22a5d17..6e56d9d8 100644 --- a/tests/Unit/MapperBuilderTest.php +++ b/tests/Unit/MapperBuilderTest.php @@ -7,7 +7,6 @@ use CuyZ\Valinor\MapperBuilder; use DateTime; use DateTimeInterface; -use LogicException; use PHPUnit\Framework\TestCase; use stdClass; @@ -44,14 +43,4 @@ public function test_mapper_instance_is_the_same(): void { self::assertSame($this->mapperBuilder->mapper(), $this->mapperBuilder->mapper()); } - - public function test_bind_with_callable_with_no_return_type_throws_exception(): void - { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('No return type was found for this callable.'); - - // @phpstan-ignore-next-line - $this->mapperBuilder->bind(static function () { - }); - } } diff --git a/tests/Unit/Utility/Reflection/ReflectionTest.php b/tests/Unit/Utility/Reflection/ReflectionTest.php index 24fde3be..1a371e2c 100644 --- a/tests/Unit/Utility/Reflection/ReflectionTest.php +++ b/tests/Unit/Utility/Reflection/ReflectionTest.php @@ -166,48 +166,4 @@ public function test_docblock_return_type_with_no_docblock_returns_null(): void self::assertNull($type); } - - public function test_reflection_of_closure_is_correct(): void - { - $callable = static function (): void { - }; - - $reflection = Reflection::ofCallable($callable); - - self::assertTrue($reflection->isClosure()); - } - - public function test_reflection_of_function_is_correct(): void - { - $reflection = Reflection::ofCallable('strlen'); - - self::assertSame('strlen', $reflection->getShortName()); - } - - public function test_reflection_of_static_method_is_correct(): void - { - $class = new class () { - public static function someMethod(): void - { - } - }; - - // @phpstan-ignore-next-line - $reflection = Reflection::ofCallable(get_class($class) . '::someMethod'); - - self::assertSame('someMethod', $reflection->getShortName()); - } - - public function test_reflection_of_callable_class_is_correct(): void - { - $class = new class () { - public function __invoke(): void - { - } - }; - - $reflection = Reflection::ofCallable($class); - - self::assertSame('__invoke', $reflection->getShortName()); - } }