diff --git a/README.md b/README.md index 8611ece..cc516a6 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,13 @@ function notify(Mailer $mailer, Logger $logger, string $to, string $subject = 'H $resolver = new Resolver($container); $args = $resolver->resolve(new ReflectionFunction('notify'), ['bob@example.com']); -// $mailer from container (by type) -// $logger from container (by type) -// $to = 'bob@example.com' (positional) -// $subject = 'Hi' (default) +// ['mailer' => Mailer, 'logger' => Logger, 'to' => 'bob@example.com', 'subject' => 'Hi'] +``` + +Results are keyed by parameter name, so you can spread them with named arguments: + +```php +notify(...$args); ``` ### Resolve with named arguments @@ -79,8 +82,8 @@ Resolver::acceptsType($reflection, LoggerInterface::class); // true/false | Method | Type | Description | |-----------------------------------------|----------|------------------------------------------------------| -| `resolve($reflection, $positional)` | instance | Resolve parameters from positional args + containers | -| `resolveNamed($reflection, $named)` | instance | Resolve from named args (priority) + containers | +| `resolve($reflection, $positional)` | instance | Resolve parameters from positional args + containers. Returns `array` keyed by parameter name | +| `resolveNamed($reflection, $named)` | instance | Resolve from named args (priority) + containers. Returns `array` keyed by parameter name | | `reflectCallable($callable)` | static | Any callable to `ReflectionFunctionAbstract` | | `toNamedArgs($reflection, $positional)` | static | Positional array to name-keyed map | | `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type | diff --git a/src/Resolver.php b/src/Resolver.php index bf0f1ec..25af17e 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -19,7 +19,6 @@ use ReflectionParameter; use function array_key_exists; -use function array_values; use function assert; use function count; use function is_a; @@ -36,12 +35,13 @@ */ final readonly class Resolver { - /** @var array */ + /** @var array */ private array $containers; + /** @param array ...$containers */ public function __construct(ContainerInterface ...$containers) { - $this->containers = array_values($containers); + $this->containers = $containers; } /** @@ -49,7 +49,7 @@ public function __construct(ContainerInterface ...$containers) * * @param array $arguments User-provided positional arguments * - * @return array Resolved arguments + * @return array|array Resolved arguments keyed by parameter name */ public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array { @@ -63,28 +63,29 @@ public function resolve(ReflectionFunctionAbstract $reflection, array $arguments $argCount = count($arguments); foreach ($params as $param) { + $paramName = $param->getName(); $typeName = self::typeName($param); // User override: positional arg of matching type beats container if ($typeName !== null && isset($arguments[$argIndex]) && $arguments[$argIndex] instanceof $typeName) { - $resolvedArgs[] = $arguments[$argIndex++]; + $resolvedArgs[$paramName] = $arguments[$argIndex++]; continue; } - [$found, $value] = $this->fromContainers($param->getName(), $typeName); + [$found, $value] = $this->fromContainers($paramName, $typeName); if ($found) { - $resolvedArgs[] = $value; + $resolvedArgs[$paramName] = $value; continue; } if ($argIndex < $argCount) { - $resolvedArgs[] = $arguments[$argIndex++]; + $resolvedArgs[$paramName] = $arguments[$argIndex++]; } elseif ($param->isDefaultValueAvailable()) { - $resolvedArgs[] = $param->getDefaultValue(); + $resolvedArgs[$paramName] = $param->getDefaultValue(); } else { - $resolvedArgs[] = null; + $resolvedArgs[$paramName] = null; } } @@ -97,7 +98,7 @@ public function resolve(ReflectionFunctionAbstract $reflection, array $arguments * * @param array $namedArgs * - * @return array Resolved arguments + * @return array Resolved arguments keyed by parameter name */ public function resolveNamed(ReflectionFunctionAbstract $reflection, array $namedArgs): array { @@ -112,19 +113,19 @@ public function resolveNamed(ReflectionFunctionAbstract $reflection, array $name $paramName = $param->getName(); if (array_key_exists($paramName, $namedArgs)) { - $resolvedArgs[] = $namedArgs[$paramName]; + $resolvedArgs[$paramName] = $namedArgs[$paramName]; continue; } [$found, $value] = $this->fromContainers($paramName, self::typeName($param)); if ($found) { - $resolvedArgs[] = $value; + $resolvedArgs[$paramName] = $value; continue; } - $resolvedArgs[] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; + $resolvedArgs[$paramName] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; } return $resolvedArgs; diff --git a/tests/unit/ResolverTest.php b/tests/unit/ResolverTest.php index 1c9b971..25ae75e 100644 --- a/tests/unit/ResolverTest.php +++ b/tests/unit/ResolverTest.php @@ -33,9 +33,9 @@ public function itShouldResolveByType(): void $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), ['hello']); - self::assertSame($service, $args[0]); - self::assertSame('hello', $args[1]); - self::assertSame(42, $args[2]); + self::assertSame($service, $args['service']); + self::assertSame('hello', $args['value']); + self::assertSame(42, $args['number']); } #[Test] @@ -48,9 +48,9 @@ public function itShouldResolveByName(): void $args = $resolver->resolve($this->constructorOf(NamedConsumer::class), []); - self::assertSame('admin', $args[0]); - self::assertSame('secret', $args[1]); - self::assertSame(3306, $args[2]); + self::assertSame('admin', $args['username']); + self::assertSame('secret', $args['password']); + self::assertSame(3306, $args['port']); } #[Test] @@ -65,9 +65,9 @@ public function itShouldTryMultipleContainers(): void $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), []); - self::assertSame($service, $args[0]); - self::assertSame('named', $args[1]); - self::assertSame(42, $args[2]); + self::assertSame($service, $args['service']); + self::assertSame('named', $args['value']); + self::assertSame(42, $args['number']); } #[Test] @@ -79,8 +79,8 @@ public function itShouldAllowUserOverride(): void $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), [$explicit, 'hello']); - self::assertSame($explicit, $args[0]); - self::assertSame('hello', $args[1]); + self::assertSame($explicit, $args['service']); + self::assertSame('hello', $args['value']); } #[Test] @@ -90,11 +90,11 @@ public function itShouldFallThroughToPositionalArgs(): void $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), ['positional']); - self::assertSame('positional', $args[0]); + self::assertSame('positional', $args['service']); } #[Test] - public function itShouldPassThroughWhenNoParams(): void + public function itShouldReturnEmptyWhenNoParams(): void { $resolver = new Resolver(new ArrayContainer()); $fn = new ReflectionFunction(static function (): void { @@ -205,9 +205,9 @@ public function itShouldResolveNamedArgsWithPrecedenceOverContainer(): void ['value' => 'explicit'], ); - self::assertSame($service, $args[0]); - self::assertSame('explicit', $args[1]); - self::assertSame(42, $args[2]); + self::assertSame($service, $args['service']); + self::assertSame('explicit', $args['value']); + self::assertSame(42, $args['number']); } #[Test] @@ -220,9 +220,9 @@ public function itShouldResolveNamedArgsFillingGapsFromContainer(): void ['username' => 'admin'], ); - self::assertSame('admin', $args[0]); - self::assertSame('auto-secret', $args[1]); - self::assertSame(3306, $args[2]); + self::assertSame('admin', $args['username']); + self::assertSame('auto-secret', $args['password']); + self::assertSame(3306, $args['port']); } #[Test] @@ -236,9 +236,9 @@ public function itShouldResolveNamedArgsWithEmptyNamedArray(): void [], ); - self::assertSame($service, $args[0]); - self::assertNull($args[1]); - self::assertSame(42, $args[2]); + self::assertSame($service, $args['service']); + self::assertNull($args['value']); + self::assertSame(42, $args['number']); } /** @param class-string $class */