feat: handle variadic parameters in constructors

Using variadic parameters is now handled properly by the library,
meaning the following example will run:

```php
final class SomeClass
{
    /** @var string[] */
    private array $values;

    public function __construct(string ...$values)
    {
        $this->values = $values;
    }
}

(new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(SomeClass::class, ['foo', 'bar', 'baz']);
```
This commit is contained in:
Romain Canon 2022-02-18 18:29:08 +01:00
parent c1a884fadd
commit b6b3296638
11 changed files with 197 additions and 13 deletions

View File

@ -17,6 +17,8 @@ final class ParameterDefinition
private bool $isOptional;
private bool $isVariadic;
/** @var mixed */
private $defaultValue;
@ -30,6 +32,7 @@ final class ParameterDefinition
string $signature,
Type $type,
bool $isOptional,
bool $isVariadic,
$defaultValue,
Attributes $attributes
) {
@ -37,6 +40,7 @@ final class ParameterDefinition
$this->signature = $signature;
$this->type = $type;
$this->isOptional = $isOptional;
$this->isVariadic = $isVariadic;
$this->defaultValue = $defaultValue;
$this->attributes = $attributes;
}
@ -61,6 +65,11 @@ final class ParameterDefinition
return $this->isOptional;
}
public function isVariadic(): bool
{
return $this->isVariadic;
}
/**
* @return mixed
*/

View File

@ -22,6 +22,7 @@ final class ParameterDefinitionCompiler
public function compile(ParameterDefinition $parameter): string
{
$isOptional = var_export($parameter->isOptional(), true);
$isVariadic = var_export($parameter->isVariadic(), true);
$defaultValue = var_export($parameter->defaultValue(), true);
$type = $this->typeCompiler->compile($parameter->type());
$attributes = $this->attributesCompiler->compile($parameter->attributes());
@ -32,6 +33,7 @@ final class ParameterDefinitionCompiler
'{$parameter->signature()}',
$type,
$isOptional,
$isVariadic,
$defaultValue,
$attributes
)

View File

@ -27,9 +27,17 @@ final class ReflectionParameterDefinitionBuilder
$signature = Reflection::signature($reflection);
$type = $typeResolver->resolveType($reflection);
$isOptional = $reflection->isOptional();
$defaultValue = $reflection->isDefaultValueAvailable() ? $reflection->getDefaultValue() : null;
$isVariadic = $reflection->isVariadic();
$attributes = $this->attributesFactory->for($reflection);
if ($reflection->isDefaultValueAvailable()) {
$defaultValue = $reflection->getDefaultValue();
} elseif ($reflection->isVariadic()) {
$defaultValue = [];
} else {
$defaultValue = null;
}
if ($isOptional
&& ! $type instanceof UnresolvableType
&& ! $type->accepts($defaultValue)
@ -37,6 +45,6 @@ final class ReflectionParameterDefinitionBuilder
throw new InvalidParameterDefaultValue($reflection, $type);
}
return new ParameterDefinition($name, $signature, $type, $isOptional, $defaultValue, $attributes);
return new ParameterDefinition($name, $signature, $type, $isOptional, $isVariadic, $defaultValue, $attributes);
}
}

View File

@ -91,6 +91,10 @@ final class ReflectionTypeResolver
$type = Reflection::flattenType($reflectionType);
if ($reflection instanceof ReflectionParameter && $reflection->isVariadic()) {
$type .= '[]';
}
return $this->parseType($type, $reflection, $this->nativeParser);
}

View File

@ -14,8 +14,6 @@ use CuyZ\Valinor\Mapper\Object\Exception\MissingMethodArgument;
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
use Exception;
use function array_values;
/** @api */
final class MethodObjectBuilder implements ObjectBuilder
{
@ -64,10 +62,20 @@ final class MethodObjectBuilder implements ObjectBuilder
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()) {
$values = [...$values, ...$arguments[$name]]; // @phpstan-ignore-line we know that the argument is iterable
} else {
$values[] = $arguments[$name];
}
}
$className = $this->class->name();
@ -75,15 +83,14 @@ final class MethodObjectBuilder implements ObjectBuilder
try {
// @PHP8.0 `array_values` can be removed
$arguments = array_values($arguments);
/** @infection-ignore-all */
$values = array_values($values);
if (! $this->method->isStatic()) {
/** @infection-ignore-all */
return new $className(...$arguments);
return new $className(...$values);
}
/** @infection-ignore-all */
return $className::$methodName(...$arguments); // @phpstan-ignore-line
return $className::$methodName(...$values); // @phpstan-ignore-line
} catch (Exception $exception) {
throw ThrowableMessage::from($exception);
}

View File

@ -22,6 +22,7 @@ final class FakeParameterDefinition
$name,
$type ?? new FakeType(),
false,
false,
null,
new FakeAttributes()
);
@ -40,6 +41,7 @@ final class FakeParameterDefinition
'Signature::' . $reflection->name,
$type,
$reflection->isOptional(),
$reflection->isVariadic(),
$reflection->isDefaultValueAvailable() ? $reflection->getDefaultValue() : null,
new FakeAttributes()
);

View File

@ -19,7 +19,15 @@ final class FakeFunctionDefinitionRepository implements FunctionDefinitionReposi
'foo',
'foo:42-1337',
new Parameters(
new ParameterDefinition('bar', 'foo::bar', NativeStringType::get(), false, 'foo', EmptyAttributes::get())
new ParameterDefinition(
'bar',
'foo::bar',
NativeStringType::get(),
false,
false,
'foo',
EmptyAttributes::get()
)
),
NativeStringType::get()
);

View File

@ -15,6 +15,7 @@ use ReflectionClass;
use function file_put_contents;
use function get_class;
use function implode;
use function sys_get_temp_dir;
use function touch;
use function unlink;
@ -36,9 +37,9 @@ final class ClassDefinitionCompilerTest extends TestCase
new class () {
public string $property = 'Some property default value';
public static function method(string $parameter = 'Some parameter default value'): string
public static function method(string $parameter = 'Some parameter default value', string ...$variadic): string
{
return $parameter;
return $parameter . implode(' / ', $variadic);
}
};
@ -79,7 +80,12 @@ final class ClassDefinitionCompilerTest extends TestCase
self::assertSame('Signature::parameter', $parameter->signature());
self::assertSame(NativeStringType::get(), $parameter->type());
self::assertTrue($parameter->isOptional());
self::assertFalse($parameter->isVariadic());
self::assertSame('Some parameter default value', $parameter->defaultValue());
$variadic = $method->parameters()->get('variadic');
self::assertTrue($variadic->isVariadic());
}
public function test_modifying_class_definition_file_invalids_compiled_class_definition(): void

View File

@ -30,7 +30,15 @@ final class FunctionDefinitionCompilerTest extends TestCase
'foo',
'foo:42-1337',
new Parameters(
new ParameterDefinition('bar', 'foo::bar', NativeStringType::get(), false, 'foo', EmptyAttributes::get())
new ParameterDefinition(
'bar',
'foo::bar',
NativeStringType::get(),
false,
false,
'foo',
EmptyAttributes::get()
)
),
NativeStringType::get()
);

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Integration\Mapping;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
final class VariadicParameterMappingTest extends IntegrationTest
{
public function test_only_variadic_parameters_are_mapped_properly(): void
{
try {
$object = $this->mapperBuilder->mapper()->map(SomeClassWithOnlyVariadicParameters::class, [
'values' => ['foo', 'bar', 'baz']
]);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertSame(['foo', 'bar', 'baz'], $object->values);
}
public function test_named_constructor_with_only_variadic_parameters_are_mapped_properly(): void
{
try {
$object = $this->mapperBuilder->mapper()->map(SomeClassWithNamedConstructorWithOnlyVariadicParameters::class, [
'values' => ['foo', 'bar', 'baz']
]);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertSame(['foo', 'bar', 'baz'], $object->values);
}
public function test_non_variadic_and_variadic_parameters_are_mapped_properly(): void
{
try {
$object = $this->mapperBuilder->mapper()->map(SomeClassWithNonVariadicAndVariadicParameters::class, [
'int' => 42,
'values' => ['foo', 'bar', 'baz']
]);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertSame(42, $object->int);
self::assertSame(['foo', 'bar', 'baz'], $object->values);
}
public function test_named_constructor_with_non_variadic_and_variadic_parameters_are_mapped_properly(): void
{
try {
$object = $this->mapperBuilder->mapper()->map(SomeClassWithNamedConstructorWithNonVariadicAndVariadicParameters::class, [
'int' => 42,
'values' => ['foo', 'bar', 'baz']
]);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertSame(42, $object->int);
self::assertSame(['foo', 'bar', 'baz'], $object->values);
}
}
final class SomeClassWithOnlyVariadicParameters
{
/** @var string[] */
public array $values;
public function __construct(string ...$values)
{
$this->values = $values;
}
}
final class SomeClassWithNamedConstructorWithOnlyVariadicParameters
{
/** @var string[] */
public array $values;
private function __construct(string ...$values)
{
$this->values = $values;
}
public static function new(string ...$values): self
{
return new self(...$values);
}
}
final class SomeClassWithNonVariadicAndVariadicParameters
{
public int $int;
/** @var string[] */
public array $values;
public function __construct(int $int, string ...$values)
{
$this->int = $int;
$this->values = $values;
}
}
final class SomeClassWithNamedConstructorWithNonVariadicAndVariadicParameters
{
public int $int;
/** @var string[] */
public array $values;
private function __construct(int $int, string ...$values)
{
$this->int = $int;
$this->values = $values;
}
public static function new(int $int, string ...$values): self
{
return new self($int, ...$values);
}
}

View File

@ -17,6 +17,7 @@ final class ParameterDefinitionTest extends TestCase
$signature = 'someParameterSignature';
$type = new FakeType();
$isOptional = true;
$isVariadic = true;
$defaultValue = 'Some parameter default value';
$attributes = new FakeAttributes();
@ -25,6 +26,7 @@ final class ParameterDefinitionTest extends TestCase
$signature,
$type,
$isOptional,
$isVariadic,
$defaultValue,
$attributes
);
@ -33,6 +35,7 @@ final class ParameterDefinitionTest extends TestCase
self::assertSame($signature, $parameter->signature());
self::assertSame($type, $parameter->type());
self::assertSame($isOptional, $parameter->isOptional());
self::assertSame($isVariadic, $parameter->isVariadic());
self::assertSame($defaultValue, $parameter->defaultValue());
self::assertSame($attributes, $parameter->attributes());
}