mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
fix: handle variadic arguments in callable constructors
This commit is contained in:
parent
e2451df2c1
commit
b646ccecf2
@ -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);
|
||||||
|
50
src/Mapper/Object/MethodArguments.php
Normal file
50
src/Mapper/Object/MethodArguments.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
30
tests/Unit/Mapper/Object/MethodArgumentsTest.php
Normal file
30
tests/Unit/Mapper/Object/MethodArgumentsTest.php
Normal 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',
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
Loading…
Reference in New Issue
Block a user