From cc53d0abcf6040ebd778c8aa1529f79c62472c88 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Thu, 25 Jun 2026 07:38:49 -0300 Subject: [PATCH] Make resolve() variadic-aware and add ParameterResolver interface resolve() now returns an ordered, ready-to-splat list (in parameter order) instead of a name-keyed array, and expands a trailing variadic parameter into all remaining positional arguments. It also accepts named arguments directly (keyed by parameter name, taking precedence over the container), so a single algorithm handles positional, named, and variadic resolution. Add a ParameterResolver interface exposing just resolve(), so consumers can depend on the contract rather than the concrete Resolver. Resolver implements it. Deprecate resolveNamed() as a thin alias of resolve(); update the README for the new return shape, precedence, variadics, and interface. --- README.md | 61 ++++++++----- src/ParameterResolver.php | 25 ++++++ src/Resolver.php | 128 +++++++++++++++------------- tests/fixtures/VariadicConsumer.php | 22 +++++ tests/unit/ResolverTest.php | 81 +++++++++++++----- 5 files changed, 216 insertions(+), 101 deletions(-) create mode 100644 src/ParameterResolver.php create mode 100644 tests/fixtures/VariadicConsumer.php diff --git a/README.md b/README.md index e547555..61a557d 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,18 @@ composer require respect/parameter ## Usage -### Resolve from a container +### Resolve arguments For each parameter the resolver tries, in order: -1. Positional argument of matching **type** -2. Container match by **type** (non-builtin) -3. Next **positional argument** -4. **Default value** -5. `null` +1. An explicit **named** argument (keyed by parameter name) +2. A **positional** argument already matching the parameter **type** +3. The **container**, matched by **type** (non-builtin) +4. The next **positional** argument +5. The parameter's **default value** +6. `null` + +A trailing **variadic** parameter receives a matching named argument (if any) followed by every remaining positional argument. ```php use Respect\Parameter\Resolver; @@ -29,25 +32,40 @@ 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' => Mailer, 'logger' => Logger, 'to' => 'bob@example.com', 'subject' => 'Hi'] +// [Mailer, Logger, 'bob@example.com', 'Hi'] — ordered, ready to splat ``` -Results are keyed by parameter name, so you can spread them with named arguments: +The result is an ordered list, so spread it straight into the call or constructor: ```php notify(...$args); +// or +$reflection->newInstanceArgs($args); ``` -### Resolve with named arguments +### Named arguments -When arguments are keyed by name (e.g. from configuration): +`resolve()` accepts named arguments too — keyed by parameter name, taking precedence over the +container; the remaining parameters are filled by type and defaults: ```php -$args = $resolver->resolveNamed( - $constructor, - ['username' => 'admin', 'password' => 'secret'], -); -// Named args take precedence, gaps filled from container by name and type +$args = $resolver->resolve($constructor, ['username' => 'admin']); +``` + +### Bind to the interface + +Type-hint `ParameterResolver` (the `resolve()` contract) rather than the concrete `Resolver` to stay +decoupled from the implementation: + +```php +use Respect\Parameter\ParameterResolver; + +final class Factory +{ + public function __construct(private ParameterResolver $resolver) + { + } +} ``` ### Reflect any callable @@ -72,12 +90,13 @@ Resolver::acceptsType($reflection, LoggerInterface::class); // true/false ## API -| Method | Type | Description | -|-----------------------------------------|----------|------------------------------------------------------| -| `resolve($reflection, $positional)` | instance | Resolve parameters from positional args + container. Returns `array` keyed by parameter name | -| `resolveNamed($reflection, $named)` | instance | Resolve from named args (priority) + container. Returns `array` keyed by parameter name | -| `reflectCallable($callable)` | static | Any callable to `ReflectionFunctionAbstract` | -| `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type | +| Method | Type | Description | +|-----------------------------------------|----------|---------------------------------------------------------------------------------------------------| +| `resolve($reflection, $arguments)` | instance | Resolve named/positional arguments + container into an ordered `list`, expanding variadics | +| `reflectCallable($callable)` | static | Any callable to `ReflectionFunctionAbstract` | +| `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type | + +`Resolver` implements `ParameterResolver`. ## License diff --git a/src/ParameterResolver.php b/src/ParameterResolver.php new file mode 100644 index 0000000..e836ac7 --- /dev/null +++ b/src/ParameterResolver.php @@ -0,0 +1,25 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Parameter; + +use ReflectionFunctionAbstract; + +interface ParameterResolver +{ + /** + * Resolve the arguments for a function/constructor into an ordered, ready-to-splat list. + * + * @param array $arguments + * + * @return list + */ + public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array; +} diff --git a/src/Resolver.php b/src/Resolver.php index f115be1..037dabb 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -19,110 +19,122 @@ use ReflectionParameter; use function array_key_exists; +use function array_values; use function assert; use function count; use function is_a; use function is_array; +use function is_int; use function is_object; use function is_string; use function str_contains; /** - * Resolves function/constructor parameters from a PSR-11 container. + * Resolves the arguments to call a function or constructor with, autowiring any parameter that is + * not supplied from a PSR-11 container by type. * - * For each parameter, tries by type (non-builtin) against the container. - * Falls through to positional arguments, then defaults. + * The result is always an ordered list ready to splat (`...$args` / `newInstanceArgs`), with + * variadic parameters expanded. */ -final readonly class Resolver +final readonly class Resolver implements ParameterResolver { public function __construct(private ContainerInterface $container) { } /** - * Resolve parameters for a function/constructor from positional arguments. + * Resolve the arguments for a function/constructor. * - * @param array $arguments User-provided positional arguments + * Provided arguments may be positional (int-keyed) or named (string-keyed by parameter name). + * For each parameter, in order: an explicit named argument wins; then a positional argument + * already matching the parameter type; then the container by type; then the next positional + * argument; then the parameter default; otherwise null. A trailing variadic parameter receives + * a matching named argument (if any) followed by every remaining positional argument. * - * @return array|array Resolved arguments keyed by parameter name + * @param array $arguments + * + * @return list */ public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array { - $params = $reflection->getParameters(); - if ($params === []) { - return $arguments; + $parameters = $reflection->getParameters(); + if ($parameters === []) { + return array_values($arguments); + } + + $positional = []; + $named = []; + foreach ($arguments as $key => $value) { + if (is_int($key)) { + $positional[] = $value; + } else { + $named[$key] = $value; + } } - $resolvedArgs = []; - $argIndex = 0; - $argCount = count($arguments); + $resolved = []; + $index = 0; + $count = count($positional); - foreach ($params as $param) { - $paramName = $param->getName(); - $typeName = self::typeName($param); + foreach ($parameters as $param) { + $name = $param->getName(); + + if ($param->isVariadic()) { + if (array_key_exists($name, $named)) { + $resolved[] = $named[$name]; + } + + while ($index < $count) { + $resolved[] = $positional[$index++]; + } - if ($typeName !== null && isset($arguments[$argIndex]) && $arguments[$argIndex] instanceof $typeName) { - $resolvedArgs[$paramName] = $arguments[$argIndex++]; + break; + } + + if (array_key_exists($name, $named)) { + $resolved[] = $named[$name]; + + continue; + } + + $type = self::typeName($param); + + if ($type !== null && isset($positional[$index]) && $positional[$index] instanceof $type) { + $resolved[] = $positional[$index++]; continue; } - if ($typeName !== null && $this->container->has($typeName)) { - $resolvedArgs[$paramName] = $this->container->get($typeName); + if ($type !== null && $this->container->has($type)) { + $resolved[] = $this->container->get($type); continue; } - if ($argIndex < $argCount) { - $resolvedArgs[$paramName] = $arguments[$argIndex++]; + if ($index < $count) { + $resolved[] = $positional[$index++]; } elseif ($param->isDefaultValueAvailable()) { - $resolvedArgs[$paramName] = $param->getDefaultValue(); + $resolved[] = $param->getDefaultValue(); } else { - $resolvedArgs[$paramName] = null; + $resolved[] = null; } } - return $resolvedArgs; + return $resolved; } /** - * Resolve parameters from explicit named args + container. - * Named args take precedence over container values. + * Resolve arguments, with named arguments taking precedence over the container. + * + * @deprecated Use {@see resolve()} instead; it now handles named arguments directly. * - * @param array $namedArgs + * @param array $arguments * - * @return array Resolved arguments keyed by parameter name + * @return list */ - public function resolveNamed(ReflectionFunctionAbstract $reflection, array $namedArgs): array + public function resolveNamed(ReflectionFunctionAbstract $reflection, array $arguments): array { - $params = $reflection->getParameters(); - if ($params === []) { - return []; - } - - $resolvedArgs = []; - - foreach ($params as $param) { - $paramName = $param->getName(); - - if (array_key_exists($paramName, $namedArgs)) { - $resolvedArgs[$paramName] = $namedArgs[$paramName]; - - continue; - } - - $typeName = self::typeName($param); - - if ($typeName !== null && $this->container->has($typeName)) { - $resolvedArgs[$paramName] = $this->container->get($typeName); - - continue; - } - - $resolvedArgs[$paramName] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; - } - - return $resolvedArgs; + return $this->resolve($reflection, $arguments); } /** Reflect any callable into its ReflectionFunctionAbstract. */ diff --git a/tests/fixtures/VariadicConsumer.php b/tests/fixtures/VariadicConsumer.php new file mode 100644 index 0000000..e699ffd --- /dev/null +++ b/tests/fixtures/VariadicConsumer.php @@ -0,0 +1,22 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Parameter\Test\Fixtures; + +final class VariadicConsumer +{ + /** @var array */ + public readonly array $numbers; + + public function __construct(public readonly SampleService $service, int ...$numbers) + { + $this->numbers = $numbers; + } +} diff --git a/tests/unit/ResolverTest.php b/tests/unit/ResolverTest.php index c3e8dd6..736c1e0 100644 --- a/tests/unit/ResolverTest.php +++ b/tests/unit/ResolverTest.php @@ -16,14 +16,22 @@ use ReflectionClass; use ReflectionFunction; use ReflectionMethod; +use Respect\Parameter\ParameterResolver; use Respect\Parameter\Resolver; use Respect\Parameter\Test\Fixtures\ArrayContainer; use Respect\Parameter\Test\Fixtures\SampleService; use Respect\Parameter\Test\Fixtures\ServiceConsumer; +use Respect\Parameter\Test\Fixtures\VariadicConsumer; #[CoversClass(Resolver::class)] final class ResolverTest extends TestCase { + #[Test] + public function itShouldImplementParameterResolver(): void + { + self::assertInstanceOf(ParameterResolver::class, new Resolver(new ArrayContainer())); + } + #[Test] public function itShouldResolveByType(): void { @@ -32,9 +40,7 @@ public function itShouldResolveByType(): void $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), ['hello']); - self::assertSame($service, $args['service']); - self::assertSame('hello', $args['value']); - self::assertSame(42, $args['number']); + self::assertSame([$service, 'hello', 42], $args); } #[Test] @@ -46,8 +52,8 @@ public function itShouldAllowUserOverride(): void $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), [$explicit, 'hello']); - self::assertSame($explicit, $args['service']); - self::assertSame('hello', $args['value']); + self::assertSame($explicit, $args[0]); + self::assertSame('hello', $args[1]); } #[Test] @@ -57,7 +63,7 @@ public function itShouldFallThroughToPositionalArgs(): void $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), ['positional']); - self::assertSame('positional', $args['service']); + self::assertSame('positional', $args[0]); } #[Test] @@ -139,35 +145,66 @@ public function itShouldReflectStaticMethodString(): void } #[Test] - public function itShouldResolveNamedArgsWithPrecedenceOverContainer(): void + public function itShouldKeepDeprecatedResolveNamedAsAnAliasOfResolve(): void + { + $resolver = new Resolver(new ArrayContainer([SampleService::class => new SampleService()])); + $constructor = $this->constructorOf(ServiceConsumer::class); + + self::assertSame( + $resolver->resolve($constructor, ['value' => 'explicit']), + $resolver->resolveNamed($constructor, ['value' => 'explicit']), + ); + } + + #[Test] + public function itShouldExpandVariadicArguments(): void { $service = new SampleService(); $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); - $args = $resolver->resolveNamed( - $this->constructorOf(ServiceConsumer::class), - ['value' => 'explicit'], - ); + $args = $resolver->resolve($this->constructorOf(VariadicConsumer::class), [1, 2, 3]); - self::assertSame($service, $args['service']); - self::assertSame('explicit', $args['value']); - self::assertSame(42, $args['number']); + self::assertSame([$service, 1, 2, 3], $args); } #[Test] - public function itShouldResolveNamedArgsWithEmptyNamedArray(): void + public function itShouldSupplyVariadicElementByName(): void { $service = new SampleService(); $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); - $args = $resolver->resolveNamed( - $this->constructorOf(ServiceConsumer::class), - [], - ); + $args = $resolver->resolve($this->constructorOf(VariadicConsumer::class), ['numbers' => 9]); + $consumer = (new ReflectionClass(VariadicConsumer::class))->newInstanceArgs($args); + + self::assertSame([$service, 9], $args); + self::assertSame([9], $consumer->numbers); + } + + #[Test] + public function itShouldResolveArgumentsReadyToSplatIntoVariadicConstructor(): void + { + $service = new SampleService(); + $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + + $args = $resolver->resolve($this->constructorOf(VariadicConsumer::class), [1, 2, 3]); + $consumer = (new ReflectionClass(VariadicConsumer::class))->newInstanceArgs($args); + + self::assertSame($service, $consumer->service); + self::assertSame([1, 2, 3], $consumer->numbers); + } + + #[Test] + public function itShouldResolveNamedArgumentsReadyToSplat(): void + { + $service = new SampleService(); + $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + + $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), ['value' => 'hi', 'number' => 7]); + $consumer = (new ReflectionClass(ServiceConsumer::class))->newInstanceArgs($args); - self::assertSame($service, $args['service']); - self::assertNull($args['value']); - self::assertSame(42, $args['number']); + self::assertSame($service, $consumer->service); + self::assertSame('hi', $consumer->value); + self::assertSame(7, $consumer->number); } /** @param class-string $class */