Skip to content

Commit

Permalink
feat!: improve object binding API
Browse files Browse the repository at this point in the history
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, [
        // …
    ]);
```
  • Loading branch information
romm committed Feb 19, 2022
1 parent 422e6a8 commit 6d42708
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 151 deletions.
24 changes: 15 additions & 9 deletions src/Library/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -102,7 +102,6 @@ public function __construct(Settings $settings)
$this->get(TypeParser::class),
),
new AttributeShellVisitor(),
new ObjectBindingShellVisitor($settings->objectBinding),
);
},

Expand Down Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion src/Library/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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> */
Expand Down
47 changes: 47 additions & 0 deletions src/Mapper/Object/CallbackObjectBuilder.php
Original file line number Diff line number Diff line change
@@ -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 changes: 76 additions & 0 deletions src/Mapper/Object/Factory/ObjectBindingBuilderFactory.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion src/Mapper/Tree/Builder/ClassNodeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
}

Expand Down
36 changes: 0 additions & 36 deletions src/Mapper/Tree/Visitor/ObjectBindingShellVisitor.php

This file was deleted.

42 changes: 25 additions & 17 deletions src/MapperBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down
21 changes: 0 additions & 21 deletions src/Utility/Reflection/Reflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>|ReflectionProperty|ReflectionFunctionAbstract $reflection
*/
Expand Down
Loading

0 comments on commit 6d42708

Please sign in to comment.