fix: handle variadic arguments in callable constructors

This commit is contained in:
Romain Canon 2022-03-17 21:54:51 +01:00
parent e2451df2c1
commit b646ccecf2
5 changed files with 85 additions and 56 deletions

View File

@ -8,8 +8,6 @@ use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage; use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
use Exception; use Exception;
use function array_values;
/** @internal */ /** @internal */
final class CallbackObjectBuilder implements ObjectBuilder final class CallbackObjectBuilder implements ObjectBuilder
{ {
@ -40,9 +38,7 @@ final class CallbackObjectBuilder implements ObjectBuilder
public function build(array $arguments): object public function build(array $arguments): object
{ {
// @PHP8.0 `array_values` can be removed $arguments = new MethodArguments($this->function->parameters(), $arguments);
/** @infection-ignore-all */
$arguments = array_values($arguments);
try { try {
return ($this->callback)(...$arguments); return ($this->callback)(...$arguments);

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Object;
use CuyZ\Valinor\Definition\Parameters;
use CuyZ\Valinor\Mapper\Object\Exception\MissingMethodArgument;
use IteratorAggregate;
use Traversable;
use function array_values;
/**
* @internal
*
* @implements IteratorAggregate<mixed>
*/
final class MethodArguments implements IteratorAggregate
{
/** @var list<mixed> */
private array $arguments = [];
/**
* @param array<string, mixed> $arguments
*/
public function __construct(Parameters $parameters, array $arguments)
{
foreach ($parameters as $parameter) {
$name = $parameter->name();
if (! array_key_exists($parameter->name(), $arguments) && ! $parameter->isOptional()) {
throw new MissingMethodArgument($parameter);
}
if ($parameter->isVariadic()) {
// @PHP8.0 remove `array_values`? Behaviour might change, careful.
$this->arguments = [...$this->arguments, ...array_values($arguments[$name])]; // @phpstan-ignore-line we know that the argument is iterable
} else {
$this->arguments[] = $arguments[$name];
}
}
}
public function getIterator(): Traversable
{
// @PHP8.0 `array_values` can be removed
yield from array_values($this->arguments);
}
}

View File

@ -10,12 +10,9 @@ use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotPublic;
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic; use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic;
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodClassReturnType; use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodClassReturnType;
use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound; use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound;
use CuyZ\Valinor\Mapper\Object\Exception\MissingMethodArgument;
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage; use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
use Exception; use Exception;
use function array_values;
/** @api */ /** @api */
final class MethodObjectBuilder implements ObjectBuilder final class MethodObjectBuilder implements ObjectBuilder
{ {
@ -64,36 +61,14 @@ final class MethodObjectBuilder implements ObjectBuilder
public function build(array $arguments): object public function build(array $arguments): object
{ {
$values = [];
foreach ($this->method->parameters() as $parameter) {
$name = $parameter->name();
if (! array_key_exists($parameter->name(), $arguments) && ! $parameter->isOptional()) {
throw new MissingMethodArgument($parameter);
}
if ($parameter->isVariadic()) {
// @PHP8.0 remove `array_values`? Behaviour might change, careful.
$values = [...$values, ...array_values($arguments[$name])]; // @phpstan-ignore-line we know that the argument is iterable
} else {
$values[] = $arguments[$name];
}
}
$className = $this->class->name(); $className = $this->class->name();
$methodName = $this->method->name(); $methodName = $this->method->name();
$arguments = new MethodArguments($this->method->parameters(), $arguments);
try { try {
// @PHP8.0 `array_values` can be removed return $this->method->isStatic()
/** @infection-ignore-all */ ? $className::$methodName(...$arguments) // @phpstan-ignore-line
$values = array_values($values); : new $className(...$arguments);
if (! $this->method->isStatic()) {
return new $className(...$values);
}
return $className::$methodName(...$values); // @phpstan-ignore-line
} catch (Exception $exception) { } catch (Exception $exception) {
throw ThrowableMessage::from($exception); throw ThrowableMessage::from($exception);
} }

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
use CuyZ\Valinor\Definition\Parameters;
use CuyZ\Valinor\Mapper\Object\Exception\MissingMethodArgument;
use CuyZ\Valinor\Mapper\Object\MethodArguments;
use CuyZ\Valinor\Tests\Fake\Definition\FakeParameterDefinition;
use PHPUnit\Framework\TestCase;
final class MethodArgumentsTest extends TestCase
{
public function test_missing_arguments_throws_exception(): void
{
$parameter = FakeParameterDefinition::new('foo');
$this->expectException(MissingMethodArgument::class);
$this->expectExceptionCode(1629468609);
$this->expectExceptionMessage("Missing argument `foo` of type `{$parameter->type()}`.");
(new MethodArguments(
new Parameters($parameter),
[
'bar' => 'bar',
]
));
}
}

View File

@ -8,7 +8,6 @@ use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotPublic;
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic; use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic;
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodClassReturnType; use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorMethodClassReturnType;
use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound; use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound;
use CuyZ\Valinor\Mapper\Object\Exception\MissingMethodArgument;
use CuyZ\Valinor\Mapper\Object\MethodObjectBuilder; use CuyZ\Valinor\Mapper\Object\MethodObjectBuilder;
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage; use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition; use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition;
@ -114,27 +113,6 @@ final class MethodObjectBuilderTest extends TestCase
new MethodObjectBuilder($class, 'invalidConstructor'); new MethodObjectBuilder($class, 'invalidConstructor');
} }
public function test_missing_arguments_throws_exception(): void
{
$object = new class ('foo') {
public string $value;
public function __construct(string $value)
{
$this->value = $value;
}
};
$class = FakeClassDefinition::fromReflection(new ReflectionClass($object));
$objectBuilder = new MethodObjectBuilder($class, '__construct');
$this->expectException(MissingMethodArgument::class);
$this->expectExceptionCode(1629468609);
$this->expectExceptionMessage('Missing argument `Signature::value` of type `string`.');
$objectBuilder->build([]);
}
public function test_exception_thrown_by_constructor_is_caught_and_wrapped(): void public function test_exception_thrown_by_constructor_is_caught_and_wrapped(): void
{ {
$class = FakeClassDefinition::fromReflection(new ReflectionClass(ObjectWithConstructorThatThrowsException::class)); $class = FakeClassDefinition::fromReflection(new ReflectionClass(ObjectWithConstructorThatThrowsException::class));