mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
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:
parent
c1a884fadd
commit
b6b3296638
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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()
|
||||
);
|
||||
|
@ -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()
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
);
|
||||
|
127
tests/Integration/Mapping/VariadicParameterMappingTest.php
Normal file
127
tests/Integration/Mapping/VariadicParameterMappingTest.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user