mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
feat!: improve object binding API
The method `MapperBuilder::bind()` can be used to define 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: ```php (new \CuyZ\Valinor\MapperBuilder()) ->bind(function(string $string, OtherClass $otherClass): SomeClass { $someClass = new SomeClass($string); $someClass->addOtherClass($otherClass); return $someClass; }) ->mapper() ->map(SomeClass::class, [ // … ]); ```
This commit is contained in:
parent
422e6a8b27
commit
6d427088f7
@ -25,6 +25,7 @@ use CuyZ\Valinor\Definition\Repository\Reflection\ReflectionFunctionDefinitionRe
|
||||
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\Builder\VisitorNodeBuilder;
|
||||
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 @@ final class Container
|
||||
$this->get(TypeParser::class),
|
||||
),
|
||||
new AttributeShellVisitor(),
|
||||
new ObjectBindingShellVisitor($settings->objectBinding),
|
||||
);
|
||||
},
|
||||
|
||||
@ -142,14 +141,21 @@ final class Container
|
||||
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(),
|
||||
|
@ -15,7 +15,7 @@ final class Settings
|
||||
/** @var array<class-string, callable(Shell): class-string> */
|
||||
public array $interfaceMapping = [];
|
||||
|
||||
/** @var array<string, callable(mixed): object> */
|
||||
/** @var list<callable> */
|
||||
public array $objectBinding = [];
|
||||
|
||||
/** @var list<callable> */
|
||||
|
47
src/Mapper/Object/CallbackObjectBuilder.php
Normal file
47
src/Mapper/Object/CallbackObjectBuilder.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
|
||||
use function array_values;
|
||||
|
||||
/** @internal */
|
||||
final class CallbackObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
private FunctionDefinition $function;
|
||||
|
||||
/** @var callable(): object */
|
||||
private $callback;
|
||||
|
||||
/**
|
||||
* @param callable(): object $callback
|
||||
*/
|
||||
public function __construct(FunctionDefinition $function, callable $callback)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
76
src/Mapper/Object/Factory/ObjectBindingBuilderFactory.php
Normal file
76
src/Mapper/Object/Factory/ObjectBindingBuilderFactory.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object\Factory;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
|
||||
use CuyZ\Valinor\Mapper\Object\CallbackObjectBuilder;
|
||||
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
||||
use CuyZ\Valinor\Mapper\Object\ObjectBuilderFilterer;
|
||||
|
||||
/** @internal */
|
||||
final class ObjectBindingBuilderFactory implements ObjectBuilderFactory
|
||||
{
|
||||
private ObjectBuilderFactory $delegate;
|
||||
|
||||
private FunctionDefinitionRepository $functionDefinitionRepository;
|
||||
|
||||
private ObjectBuilderFilterer $objectBuilderFilterer;
|
||||
|
||||
/** @var list<callable> */
|
||||
private array $callbacks;
|
||||
|
||||
/** @var list<FunctionDefinition> */
|
||||
private array $functions;
|
||||
|
||||
/**
|
||||
* @param list<callable> $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;
|
||||
}
|
||||
}
|
@ -122,7 +122,7 @@ final class ClassNodeBuilder implements NodeBuilder
|
||||
*/
|
||||
private function transformSource($source, Argument ...$arguments): array
|
||||
{
|
||||
if ($source === null) {
|
||||
if ($source === null || count($arguments) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Visitor;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||
|
||||
/** @internal */
|
||||
final class ObjectBindingShellVisitor implements ShellVisitor
|
||||
{
|
||||
/** @var array<string, callable(mixed): object> */
|
||||
private array $callbacks;
|
||||
|
||||
/**
|
||||
* @param array<string, callable(mixed): object> $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);
|
||||
}
|
||||
}
|
@ -9,8 +9,6 @@ use CuyZ\Valinor\Library\Settings;
|
||||
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 @@ final class MapperBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
@ -173,27 +173,6 @@ final class Reflection
|
||||
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<object>|ReflectionProperty|ReflectionFunctionAbstract $reflection
|
||||
*/
|
||||
|
@ -8,7 +8,6 @@ use CuyZ\Valinor\Mapper\MappingError;
|
||||
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use stdClass;
|
||||
|
||||
use function get_class;
|
||||
@ -26,9 +25,7 @@ final class ObjectBindingMappingTest extends IntegrationTest
|
||||
$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 @@ final class ObjectBindingMappingTest extends IntegrationTest
|
||||
$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 @@ final class ObjectBindingMappingTest extends IntegrationTest
|
||||
->bind(fn (): DateTimeImmutable => $defaultImmutable)
|
||||
->mapper()
|
||||
->map(SimpleDateTimeValues::class, [
|
||||
'dateTimeInterface' => 1357047105,
|
||||
'dateTimeImmutable' => 1357047105,
|
||||
'dateTime' => 1357047105,
|
||||
]);
|
||||
@ -76,16 +70,58 @@ final class ObjectBindingMappingTest extends IntegrationTest
|
||||
$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;
|
||||
|
@ -7,7 +7,6 @@ namespace CuyZ\Valinor\Tests\Unit;
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use LogicException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use stdClass;
|
||||
|
||||
@ -44,14 +43,4 @@ final class MapperBuilderTest extends TestCase
|
||||
{
|
||||
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 () {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -166,48 +166,4 @@ final class ReflectionTest extends TestCase
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user