Skip to content

Commit

Permalink
feat: handle variadic parameters in constructors
Browse files Browse the repository at this point in the history
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']);
```
  • Loading branch information
romm committed Feb 19, 2022
1 parent c1a884f commit b6b3296
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 13 deletions.
9 changes: 9 additions & 0 deletions src/Definition/ParameterDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ final class ParameterDefinition

private bool $isOptional;

private bool $isVariadic;

/** @var mixed */
private $defaultValue;

Expand All @@ -30,13 +32,15 @@ public function __construct(
string $signature,
Type $type,
bool $isOptional,
bool $isVariadic,
$defaultValue,
Attributes $attributes
) {
$this->name = $name;
$this->signature = $signature;
$this->type = $type;
$this->isOptional = $isOptional;
$this->isVariadic = $isVariadic;
$this->defaultValue = $defaultValue;
$this->attributes = $attributes;
}
Expand All @@ -61,6 +65,11 @@ public function isOptional(): bool
return $this->isOptional;
}

public function isVariadic(): bool
{
return $this->isVariadic;
}

/**
* @return mixed
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function __construct(TypeCompiler $typeCompiler, AttributesCompiler $attr
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());
Expand All @@ -32,6 +33,7 @@ public function compile(ParameterDefinition $parameter): string
'{$parameter->signature()}',
$type,
$isOptional,
$isVariadic,
$defaultValue,
$attributes
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,24 @@ public function for(ReflectionParameter $reflection, ReflectionTypeResolver $typ
$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)
) {
throw new InvalidParameterDefaultValue($reflection, $type);
}

return new ParameterDefinition($name, $signature, $type, $isOptional, $defaultValue, $attributes);
return new ParameterDefinition($name, $signature, $type, $isOptional, $isVariadic, $defaultValue, $attributes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ private function nativeType(Reflector $reflection): ?Type

$type = Reflection::flattenType($reflectionType);

if ($reflection instanceof ReflectionParameter && $reflection->isVariadic()) {
$type .= '[]';
}

return $this->parseType($type, $reflection, $this->nativeParser);
}

Expand Down
21 changes: 14 additions & 7 deletions src/Mapper/Object/MethodObjectBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
use CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage;
use Exception;

use function array_values;

/** @api */
final class MethodObjectBuilder implements ObjectBuilder
{
Expand Down Expand Up @@ -64,26 +62,35 @@ public function describeArguments(): iterable

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();
$methodName = $this->method->name();

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);
}
Expand Down
2 changes: 2 additions & 0 deletions tests/Fake/Definition/FakeParameterDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static function new(string $name = 'someParameter', Type $type = null): P
$name,
$type ?? new FakeType(),
false,
false,
null,
new FakeAttributes()
);
Expand All @@ -40,6 +41,7 @@ public static function fromReflection(ReflectionParameter $reflection): Paramete
'Signature::' . $reflection->name,
$type,
$reflection->isOptional(),
$reflection->isVariadic(),
$reflection->isDefaultValueAvailable() ? $reflection->getDefaultValue() : null,
new FakeAttributes()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ public function for(callable $function): FunctionDefinition
'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()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use function file_put_contents;
use function get_class;
use function implode;
use function sys_get_temp_dir;
use function touch;
use function unlink;
Expand All @@ -36,9 +37,9 @@ public function test_class_definition_is_compiled_correctly(): void
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);
}
};

Expand Down Expand Up @@ -79,7 +80,12 @@ public static function method(string $parameter = 'Some parameter default value'
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ public function test_function_is_compiled_correctly(): void
'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()
);
Expand Down
127 changes: 127 additions & 0 deletions tests/Integration/Mapping/VariadicParameterMappingTest.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions tests/Unit/Definition/ParameterDefinitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function test_parameter_data_can_be_retrieved(): void
$signature = 'someParameterSignature';
$type = new FakeType();
$isOptional = true;
$isVariadic = true;
$defaultValue = 'Some parameter default value';
$attributes = new FakeAttributes();

Expand All @@ -25,6 +26,7 @@ public function test_parameter_data_can_be_retrieved(): void
$signature,
$type,
$isOptional,
$isVariadic,
$defaultValue,
$attributes
);
Expand All @@ -33,6 +35,7 @@ public function test_parameter_data_can_be_retrieved(): void
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());
}
Expand Down

0 comments on commit b6b3296

Please sign in to comment.