Skip to content

Commit

Permalink
In methods of array definitions add autowiring and improve variadic a…
Browse files Browse the repository at this point in the history
…rguments support (#44)
  • Loading branch information
vjik committed Oct 31, 2022
1 parent 9ce8320 commit 6b8c8b2
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 31 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -2,7 +2,7 @@

## 2.1.1 under development

- no changes in this release.
- Enh #44: In methods of array definitions add autowiring and improve variadic arguments support (@vjik)

## 2.1.0 October 25, 2022

Expand Down
81 changes: 53 additions & 28 deletions src/ArrayDefinition.php
Expand Up @@ -6,7 +6,9 @@

use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use ReflectionMethod;
use Yiisoft\Definitions\Contract\DefinitionInterface;
use Yiisoft\Definitions\Contract\ReferenceInterface;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Helpers\DefinitionExtractor;
use Yiisoft\Definitions\Helpers\DefinitionResolver;
Expand Down Expand Up @@ -137,50 +139,56 @@ public function getMethodsAndProperties(): array

public function resolve(ContainerInterface $container): object
{
$class = $this->getClass();
$dependencies = DefinitionExtractor::fromClassName($class);
$constructorArguments = $this->getConstructorArguments();
$class = $this->class;

$this->injectArguments($dependencies, $constructorArguments);

/** @psalm-suppress MixedArgumentTypeCoercion */
$resolved = DefinitionResolver::resolveArray($container, $this->referenceContainer, $dependencies);
$resolvedConstructorArguments = $this->resolveFunctionArguments(
$container,
DefinitionExtractor::fromClassName($class),
$this->getConstructorArguments()
);

/** @psalm-suppress MixedMethodCall */
$object = new $class(...array_values($resolved));
$object = new $class(...$resolvedConstructorArguments);

foreach ($this->getMethodsAndProperties() as $item) {
/** @var mixed $value */
[$type, $name, $value] = $item;
/** @var mixed */
$value = DefinitionResolver::resolve($container, $this->referenceContainer, $value);
if ($type === self::TYPE_METHOD) {
/** @var mixed */
$setter = call_user_func_array([$object, $name], $value);
/** @var array $value */
$resolvedMethodArguments = $this->resolveFunctionArguments(
$container,
DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)),
$value,
);
/** @var mixed $setter */
$setter = call_user_func_array([$object, $name], $resolvedMethodArguments);
if ($setter instanceof $object) {
/** @var object */
/** @var object $object */
$object = $setter;
}
} elseif ($type === self::TYPE_PROPERTY) {
$object->$name = $value;
$object->$name = DefinitionResolver::resolve($container, $this->referenceContainer, $value);
}
}

return $object;
}

/**
* @psalm-param array<string, ParameterDefinition> $dependencies
* @psalm-param-out array<array-key, Yiisoft\Definitions\ParameterDefinition|mixed> $dependencies
* @param array<string,ParameterDefinition> $dependencies
*
* @throws InvalidConfigException
* @psalm-return list<mixed>
*/
private function injectArguments(array &$dependencies, array $arguments): void
{
private function resolveFunctionArguments(
ContainerInterface $container,
array $dependencies,
array $arguments
): array {
$isIntegerIndexed = $this->isIntegerIndexed($arguments);
$dependencyIndex = 0;
$usedArguments = [];
$variadicKey = null;

foreach ($dependencies as $key => &$value) {
if ($value->isVariadic()) {
$variadicKey = $key;
Expand All @@ -193,24 +201,41 @@ private function injectArguments(array &$dependencies, array $arguments): void
$dependencyIndex++;
}
unset($value);

if ($variadicKey !== null) {
if (!$isIntegerIndexed && isset($arguments[$variadicKey])) {
if ($arguments[$variadicKey] instanceof ReferenceInterface) {
/** @var mixed */
$arguments[$variadicKey] = DefinitionResolver::resolve(
$container,
$this->referenceContainer,
$arguments[$variadicKey]
);
}

if (is_array($arguments[$variadicKey])) {
unset($dependencies[$variadicKey]);
$dependencies += $arguments[$variadicKey];
return;
} else {
throw new InvalidArgumentException(
sprintf(
'Named argument for a variadic parameter should be an array, "%s" given.',
gettype($arguments[$variadicKey])
)
);
}

throw new InvalidArgumentException(sprintf('Named argument for a variadic parameter should be an array, "%s" given.', gettype($arguments[$variadicKey])));
}

/** @var mixed $value */
foreach ($arguments as $index => $value) {
if (!isset($usedArguments[$index])) {
$dependencies[$index] = DefinitionResolver::ensureResolvable($value);
} else {
/** @var mixed $value */
foreach ($arguments as $index => $value) {
if (!isset($usedArguments[$index])) {
$dependencies[$index] = DefinitionResolver::ensureResolvable($value);
}
}
}
}

$resolvedArguments = DefinitionResolver::resolveArray($container, $this->referenceContainer, $dependencies);
return array_values($resolvedArguments);
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/Helpers/DefinitionResolver.php
Expand Up @@ -23,7 +23,7 @@ final class DefinitionResolver
*
* @param ContainerInterface $container Container to get dependencies from.
* @param ContainerInterface|null $referenceContainer Container to get references from.
* @psalm-param array<string,mixed> $definitions Definitions to resolve.
* @param array $definitions Definitions to resolve.
*
* @return array The resolved dependencies.
*/
Expand Down Expand Up @@ -65,7 +65,6 @@ public static function resolve(ContainerInterface $container, ?ContainerInterfac
/** @var mixed $definition */
$definition = $definition->resolve($container);
} elseif (is_array($definition)) {
/** @psalm-var array<string,mixed> $definition */
return self::resolveArray($container, $referenceContainer, $definition);
}

Expand Down
39 changes: 39 additions & 0 deletions tests/Support/Mouse.php
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Definitions\Tests\Support;

final class Mouse
{
private ?string $name = null;
private ?EngineInterface $engine = null;
private array $colors = [];

public function setNameAndEngine(string $name, EngineInterface $engine): void
{
$this->name = $name;
$this->engine = $engine;
}

public function setNameAndColors(string $name, ...$colors): void
{
$this->name = $name;
$this->colors = $colors;
}

public function getName(): ?string
{
return $this->name;
}

public function getEngine(): ?EngineInterface
{
return $this->engine;
}

public function getColors(): array
{
return $this->colors;
}
}
176 changes: 176 additions & 0 deletions tests/Unit/ArrayDefinitionTest.php
Expand Up @@ -15,6 +15,7 @@
use Yiisoft\Definitions\Tests\Support\EngineInterface;
use Yiisoft\Definitions\Tests\Support\EngineMarkOne;
use Yiisoft\Definitions\Tests\Support\EngineMarkTwo;
use Yiisoft\Definitions\Tests\Support\Mouse;
use Yiisoft\Definitions\Tests\Support\Phone;
use Yiisoft\Test\Support\Container\SimpleContainer;

Expand Down Expand Up @@ -245,6 +246,181 @@ public function testCallFluentMethod(): void
self::assertSame($country, $phone->getCountry());
}

public function dataMethodAutowiring(): array
{
return [
[
'kitty',
EngineMarkOne::class,
['kitty'],
],
[
'kitty',
EngineMarkOne::class,
['name' => 'kitty'],
],
[
'kitty',
EngineMarkTwo::class,
['kitty', new EngineMarkTwo()],
],
[
'kitty',
EngineMarkTwo::class,
['name' => 'kitty', 'engine' => new EngineMarkTwo()],
],
[
'kitty',
EngineMarkTwo::class,
['kitty', Reference::to('mark2')],
],
];
}

/**
* @dataProvider dataMethodAutowiring
*/
public function testMethodAutowiring(?string $expectedName, ?string $expectedEngine, array $data): void
{
$container = new SimpleContainer([
EngineInterface::class => new EngineMarkOne(),
'mark2' => new EngineMarkTwo(),
]);

$definition = ArrayDefinition::fromConfig([
ArrayDefinition::CLASS_NAME => Mouse::class,
'setNameAndEngine()' => $data,
]);

/** @var Mouse $mouse */
$mouse = $definition->resolve($container);

self::assertSame($expectedName, $mouse->getName());
self::assertInstanceOf($expectedEngine, $mouse->getEngine());
}

public function dataMethodVariadic(): array
{
return [
[
'kitty',
[],
['kitty'],
],
[
'kitty',
[],
['name' => 'kitty'],
],
[
'kitty',
[],
['name' => 'kitty', 'colors' => []],
],
[
'kitty',
[1, 2, 3],
['name' => 'kitty', 'colors' => [1, 2, 3]],
],
[
'kitty',
[1, 2, 3],
['kitty', 1, 2, 3],
],
[
'kitty',
[[1, 2, 3]],
['kitty', [1, 2, 3]],
],
[
'kitty',
[1, 2, 3],
['name' => 'kitty', 'colors' => Reference::to('data')],
['data' => [1, 2, 3]],
],
[
'kitty',
[[1, 2, 3]],
['kitty', Reference::to('data')],
['data' => [1, 2, 3]],
],
];
}

/**
* @dataProvider dataMethodVariadic
*/
public function testMethodVariadic(
?string $expectedName,
array $expectedColors,
array $data,
array $containerDefinitions = []
): void {
$container = new SimpleContainer($containerDefinitions);

$definition = ArrayDefinition::fromConfig([
ArrayDefinition::CLASS_NAME => Mouse::class,
'setNameAndColors()' => $data,
]);

/** @var Mouse $mouse */
$mouse = $definition->resolve($container);

self::assertSame($expectedName, $mouse->getName());
self::assertSame($expectedColors, $mouse->getColors());
}

public function testArgumentsIndexedBothByNameAndByPositionInMethod(): void
{
$definition = ArrayDefinition::fromConfig([
ArrayDefinition::CLASS_NAME => Mouse::class,
'setNameAndEngine()' => ['kitty', 'engine' => new EngineMarkOne()],
]);
$container = new SimpleContainer();

$this->expectException(InvalidConfigException::class);
$this->expectExceptionMessage(
'Arguments indexed both by name and by position are not allowed in the same array.'
);
$definition->resolve($container);
}

public function testMethodWithWrongVariadicArgument(): void
{
$container = new SimpleContainer();

$definition = ArrayDefinition::fromConfig([
ArrayDefinition::CLASS_NAME => Mouse::class,
'setNameAndColors()' => [
'name' => 'kitty',
'colors' => 'red',
],
]);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Named argument for a variadic parameter should be an array, "string" given.');
$definition->resolve($container);
}

public function testMethodWithWrongReferenceVariadicArgument(): void
{
$container = new SimpleContainer([
'data' => 32,
]);

$definition = ArrayDefinition::fromConfig([
ArrayDefinition::CLASS_NAME => Mouse::class,
'setNameAndColors()' => [
'name' => 'kitty',
'colors' => Reference::to('data'),
],
]);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Named argument for a variadic parameter should be an array, "integer" given.');
$definition->resolve($container);
}

public function testMerge(): void
{
$a = ArrayDefinition::fromConfig([
Expand Down

0 comments on commit 6b8c8b2

Please sign in to comment.