fix: properly handle callable objects of the same class

Using two instances of the same class implementing the `__invoke()`
method in one of the mapper builder methods will now be properly handled
by the library
This commit is contained in:
Romain Canon 2022-08-02 11:01:28 +02:00
parent 444747ab0a
commit ae7ddcf3ca
11 changed files with 71 additions and 122 deletions

View File

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Exception;
use CuyZ\Valinor\Definition\FunctionDefinition;
use RuntimeException;
/** @internal */
final class CallbackNotFound extends RuntimeException
{
public function __construct(FunctionDefinition $function)
{
parent::__construct(
"The callback associated to `{$function->signature()}` could not be found.",
1647523495
);
}
}

View File

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Exception;
use RuntimeException;
/** @internal */
final class FunctionNotFound extends RuntimeException
{
/**
* @param string|int $key
*/
public function __construct($key)
{
parent::__construct(
"The function `$key` was not found.",
1647523444
);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
/** @internal */
final class FunctionObject
{
private FunctionDefinition $definition;
/** @var callable */
private $callback;
public function __construct(FunctionDefinition $definition, callable $callback)
{
$this->definition = $definition;
$this->callback = $callback;
}
public function definition(): FunctionDefinition
{
return $this->definition;
}
public function callback(): callable
{
return $this->callback;
}
}

View File

@ -4,8 +4,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use CuyZ\Valinor\Definition\Exception\CallbackNotFound;
use CuyZ\Valinor\Definition\Exception\FunctionNotFound;
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
use IteratorAggregate;
use Traversable;
@ -15,7 +13,7 @@ use function array_keys;
/**
* @internal
*
* @implements IteratorAggregate<string|int, FunctionDefinition>
* @implements IteratorAggregate<string|int, FunctionObject>
*/
final class FunctionsContainer implements IteratorAggregate
{
@ -24,7 +22,7 @@ final class FunctionsContainer implements IteratorAggregate
/** @var array<callable> */
private array $callables;
/** @var array<array{definition: FunctionDefinition, callback: callable}> */
/** @var array<FunctionObject> */
private array $functions = [];
/**
@ -47,43 +45,26 @@ final class FunctionsContainer implements IteratorAggregate
/**
* @param string|int $key
*/
public function get($key): FunctionDefinition
public function get($key): FunctionObject
{
if (! $this->has($key)) {
throw new FunctionNotFound($key);
}
return $this->function($key)['definition'];
}
public function callback(FunctionDefinition $function): callable
{
foreach ($this->functions as $data) {
if ($function === $data['definition']) {
return $data['callback'];
}
}
throw new CallbackNotFound($function);
return $this->function($key);
}
public function getIterator(): Traversable
{
foreach (array_keys($this->callables) as $key) {
yield $key => $this->function($key)['definition'];
yield $key => $this->function($key);
}
}
/**
* @param string|int $key
* @return array{definition: FunctionDefinition, callback: callable}
*/
private function function($key): array
private function function($key): FunctionObject
{
/** @infection-ignore-all */
return $this->functions[$key] ??= [
'callback' => $this->callables[$key],
'definition' => $this->functionDefinitionRepository->for($this->callables[$key]),
];
return $this->functions[$key] ??= new FunctionObject(
$this->functionDefinitionRepository->for($this->callables[$key]),
$this->callables[$key]
);
}
}

View File

@ -70,8 +70,8 @@ final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
$methods = $class->methods();
foreach ($this->constructors as $constructor) {
if ($constructor->returnType()->matches($type)) {
$builders[] = new FunctionObjectBuilder($constructor, $this->constructors->callback($constructor));
if ($constructor->definition()->returnType()->matches($type)) {
$builders[] = new FunctionObjectBuilder($constructor);
}
}

View File

@ -56,15 +56,17 @@ final class DateTimeObjectBuilderFactory implements ObjectBuilderFactory
$this->builders[$key] = [];
foreach ($this->functions as $function) {
if (! $function->returnType()->matches($type)) {
$definition = $function->definition();
if (! $definition->returnType()->matches($type)) {
continue;
}
if (count($function->parameters()) === 1) {
if (count($definition->parameters()) === 1) {
$overridesDefault = true;
}
$this->builders[$key][] = new FunctionObjectBuilder($function, $this->functions->callback($function));
$this->builders[$key][] = new FunctionObjectBuilder($function);
}
if (! $overridesDefault) {

View File

@ -4,40 +4,33 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Object;
use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Definition\FunctionObject;
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
use Exception;
/** @internal */
final class FunctionObjectBuilder implements ObjectBuilder
{
private FunctionDefinition $function;
/** @var callable(): object */
private $callback;
private FunctionObject $function;
private Arguments $arguments;
/**
* @param callable(): object $callback
*/
public function __construct(FunctionDefinition $function, callable $callback)
public function __construct(FunctionObject $function)
{
$this->function = $function;
$this->callback = $callback;
}
public function describeArguments(): Arguments
{
return $this->arguments ??= Arguments::fromParameters($this->function->parameters());
return $this->arguments ??= Arguments::fromParameters($this->function->definition()->parameters());
}
public function build(array $arguments): object
{
$arguments = new MethodArguments($this->function->parameters(), $arguments);
$arguments = new MethodArguments($this->function->definition()->parameters(), $arguments);
try {
return ($this->callback)(...$arguments);
return ($this->function->callback())(...$arguments);
} catch (Exception $exception) {
throw UserlandError::from($exception);
}
@ -45,6 +38,6 @@ final class FunctionObjectBuilder implements ObjectBuilder
public function signature(): string
{
return $this->function->signature();
return $this->function->definition()->signature();
}
}

View File

@ -49,7 +49,7 @@ final class ObjectImplementations
throw new CannotResolveObjectType($name);
}
return $this->functions->get($name);
return $this->functions->get($name)->definition();
}
/**
@ -72,11 +72,8 @@ final class ObjectImplementations
*/
private function call(string $name, array $arguments): string
{
$function = $this->functions->get($name);
$callback = $this->functions->callback($function);
try {
$signature = $callback(...$arguments);
$signature = ($this->functions->get($name)->callback())(...$arguments);
} catch (Exception $exception) {
throw new ObjectImplementationCallbackError($name, $exception);
}
@ -93,7 +90,7 @@ final class ObjectImplementations
*/
private function implementations(string $name): array
{
$function = $this->functions->get($name);
$function = $this->functions->get($name)->definition();
try {
$type = $this->typeParser->parse($name);

View File

@ -31,7 +31,7 @@ final class ValueAlteringNodeBuilder implements NodeBuilder
$value = $node->value();
foreach ($this->functions as $function) {
$parameters = $function->parameters();
$parameters = $function->definition()->parameters();
if (count($parameters) === 0) {
continue;
@ -43,7 +43,7 @@ final class ValueAlteringNodeBuilder implements NodeBuilder
continue;
}
$value = ($this->functions->callback($function))($value);
$value = ($function->callback())($value);
$node = $node->withValue($value);
}

View File

@ -4,10 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Definition;
use CuyZ\Valinor\Definition\Exception\CallbackNotFound;
use CuyZ\Valinor\Definition\Exception\FunctionNotFound;
use CuyZ\Valinor\Definition\FunctionsContainer;
use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition;
use CuyZ\Valinor\Tests\Fake\Definition\Repository\FakeFunctionDefinitionRepository;
use PHPUnit\Framework\TestCase;
@ -15,26 +12,6 @@ use function iterator_to_array;
final class FunctionsContainerTest extends TestCase
{
public function test_get_unknown_function_throws_exception(): void
{
$this->expectException(FunctionNotFound::class);
$this->expectExceptionCode(1647523444);
$this->expectExceptionMessage('The function `unknown` was not found.');
(new FunctionsContainer(new FakeFunctionDefinitionRepository(), []))->get('unknown');
}
public function test_get_unknown_callback_throws_exception(): void
{
$function = FakeFunctionDefinition::new();
$this->expectException(CallbackNotFound::class);
$this->expectExceptionCode(1647523495);
$this->expectExceptionMessage("The callback associated to `{$function->signature()}` could not be found.");
(new FunctionsContainer(new FakeFunctionDefinitionRepository(), []))->callback($function);
}
public function test_keys_are_kept_when_iterating(): void
{
$functions = (new FunctionsContainer(new FakeFunctionDefinitionRepository(), [
@ -47,4 +24,14 @@ final class FunctionsContainerTest extends TestCase
self::assertArrayHasKey('foo', $functions);
self::assertArrayHasKey('bar', $functions);
}
public function test_function_object_remains_the_same(): void
{
$functions = (new FunctionsContainer(new FakeFunctionDefinitionRepository(), [fn () => 'foo']));
$functionA = $functions->get(0);
$functionB = $functions->get(0);
self::assertSame($functionA, $functionB);
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
use CuyZ\Valinor\Definition\FunctionObject;
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition;
use PHPUnit\Framework\TestCase;
@ -13,7 +14,7 @@ final class FunctionObjectBuilderTest extends TestCase
{
public function test_arguments_instance_stays_the_same(): void
{
$objectBuilder = new FunctionObjectBuilder(FakeFunctionDefinition::new(), fn () => new stdClass());
$objectBuilder = new FunctionObjectBuilder(new FunctionObject(FakeFunctionDefinition::new(), fn () => new stdClass()));
$argumentsA = $objectBuilder->describeArguments();
$argumentsB = $objectBuilder->describeArguments();