Skip to content

Commit e8b5025

Browse files
committed
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.
1 parent 7245b3b commit e8b5025

5 files changed

Lines changed: 216 additions & 101 deletions

File tree

README.md

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ composer require respect/parameter
1010

1111
## Usage
1212

13-
### Resolve from a container
13+
### Resolve arguments
1414

1515
For each parameter the resolver tries, in order:
1616

17-
1. Positional argument of matching **type**
18-
2. Container match by **type** (non-builtin)
19-
3. Next **positional argument**
20-
4. **Default value**
21-
5. `null`
17+
1. An explicit **named** argument (keyed by parameter name)
18+
2. A **positional** argument already matching the parameter **type**
19+
3. The **container**, matched by **type** (non-builtin)
20+
4. The next **positional** argument
21+
5. The parameter's **default value**
22+
6. `null`
23+
24+
A trailing **variadic** parameter receives a matching named argument (if any) followed by every remaining positional argument.
2225

2326
```php
2427
use Respect\Parameter\Resolver;
@@ -29,25 +32,40 @@ function notify(Mailer $mailer, Logger $logger, string $to, string $subject = 'H
2932

3033
$resolver = new Resolver($container);
3134
$args = $resolver->resolve(new ReflectionFunction('notify'), ['bob@example.com']);
32-
// ['mailer' => Mailer, 'logger' => Logger, 'to' => 'bob@example.com', 'subject' => 'Hi']
35+
// [Mailer, Logger, 'bob@example.com', 'Hi'] — ordered, ready to splat
3336
```
3437

35-
Results are keyed by parameter name, so you can spread them with named arguments:
38+
The result is an ordered list, so spread it straight into the call or constructor:
3639

3740
```php
3841
notify(...$args);
42+
// or
43+
$reflection->newInstanceArgs($args);
3944
```
4045

41-
### Resolve with named arguments
46+
### Named arguments
4247

43-
When arguments are keyed by name (e.g. from configuration):
48+
`resolve()` accepts named arguments too — keyed by parameter name, taking precedence over the
49+
container; the remaining parameters are filled by type and defaults:
4450

4551
```php
46-
$args = $resolver->resolveNamed(
47-
$constructor,
48-
['username' => 'admin', 'password' => 'secret'],
49-
);
50-
// Named args take precedence, gaps filled from container by name and type
52+
$args = $resolver->resolve($constructor, ['username' => 'admin']);
53+
```
54+
55+
### Bind to the interface
56+
57+
Type-hint `ParameterResolver` (the `resolve()` contract) rather than the concrete `Resolver` to stay
58+
decoupled from the implementation:
59+
60+
```php
61+
use Respect\Parameter\ParameterResolver;
62+
63+
final class Factory
64+
{
65+
public function __construct(private ParameterResolver $resolver)
66+
{
67+
}
68+
}
5169
```
5270

5371
### Reflect any callable
@@ -72,12 +90,13 @@ Resolver::acceptsType($reflection, LoggerInterface::class); // true/false
7290

7391
## API
7492

75-
| Method | Type | Description |
76-
|-----------------------------------------|----------|------------------------------------------------------|
77-
| `resolve($reflection, $positional)` | instance | Resolve parameters from positional args + container. Returns `array<string, mixed>` keyed by parameter name |
78-
| `resolveNamed($reflection, $named)` | instance | Resolve from named args (priority) + container. Returns `array<string, mixed>` keyed by parameter name |
79-
| `reflectCallable($callable)` | static | Any callable to `ReflectionFunctionAbstract` |
80-
| `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type |
93+
| Method | Type | Description |
94+
|-----------------------------------------|----------|---------------------------------------------------------------------------------------------------|
95+
| `resolve($reflection, $arguments)` | instance | Resolve named/positional arguments + container into an ordered `list<mixed>`, expanding variadics |
96+
| `reflectCallable($callable)` | static | Any callable to `ReflectionFunctionAbstract` |
97+
| `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type |
98+
99+
`Resolver` implements `ParameterResolver`.
81100

82101
## License
83102

src/ParameterResolver.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: ISC
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Parameter;
12+
13+
use ReflectionFunctionAbstract;
14+
15+
interface ParameterResolver
16+
{
17+
/**
18+
* Resolve the arguments for a function/constructor into an ordered, ready-to-splat list.
19+
*
20+
* @param array<int|string, mixed> $arguments
21+
*
22+
* @return list<mixed>
23+
*/
24+
public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array;
25+
}

src/Resolver.php

Lines changed: 70 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -19,110 +19,122 @@
1919
use ReflectionParameter;
2020

2121
use function array_key_exists;
22+
use function array_values;
2223
use function assert;
2324
use function count;
2425
use function is_a;
2526
use function is_array;
27+
use function is_int;
2628
use function is_object;
2729
use function is_string;
2830
use function str_contains;
2931

3032
/**
31-
* Resolves function/constructor parameters from a PSR-11 container.
33+
* Resolves the arguments to call a function or constructor with, autowiring any parameter that is
34+
* not supplied from a PSR-11 container by type.
3235
*
33-
* For each parameter, tries by type (non-builtin) against the container.
34-
* Falls through to positional arguments, then defaults.
36+
* The result is always an ordered list ready to splat (`...$args` / `newInstanceArgs`), with
37+
* variadic parameters expanded.
3538
*/
36-
final readonly class Resolver
39+
final readonly class Resolver implements ParameterResolver
3740
{
3841
public function __construct(private ContainerInterface $container)
3942
{
4043
}
4144

4245
/**
43-
* Resolve parameters for a function/constructor from positional arguments.
46+
* Resolve the arguments for a function/constructor.
4447
*
45-
* @param array<int, mixed> $arguments User-provided positional arguments
48+
* Provided arguments may be positional (int-keyed) or named (string-keyed by parameter name).
49+
* For each parameter, in order: an explicit named argument wins; then a positional argument
50+
* already matching the parameter type; then the container by type; then the next positional
51+
* argument; then the parameter default; otherwise null. A trailing variadic parameter receives
52+
* a matching named argument (if any) followed by every remaining positional argument.
4653
*
47-
* @return array<int, mixed>|array<string, mixed> Resolved arguments keyed by parameter name
54+
* @param array<int|string, mixed> $arguments
55+
*
56+
* @return list<mixed>
4857
*/
4958
public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array
5059
{
51-
$params = $reflection->getParameters();
52-
if ($params === []) {
53-
return $arguments;
60+
$parameters = $reflection->getParameters();
61+
if ($parameters === []) {
62+
return array_values($arguments);
63+
}
64+
65+
$positional = [];
66+
$named = [];
67+
foreach ($arguments as $key => $value) {
68+
if (is_int($key)) {
69+
$positional[] = $value;
70+
} else {
71+
$named[$key] = $value;
72+
}
5473
}
5574

56-
$resolvedArgs = [];
57-
$argIndex = 0;
58-
$argCount = count($arguments);
75+
$resolved = [];
76+
$index = 0;
77+
$count = count($positional);
5978

60-
foreach ($params as $param) {
61-
$paramName = $param->getName();
62-
$typeName = self::typeName($param);
79+
foreach ($parameters as $param) {
80+
$name = $param->getName();
81+
82+
if ($param->isVariadic()) {
83+
if (array_key_exists($name, $named)) {
84+
$resolved[] = $named[$name];
85+
}
86+
87+
while ($index < $count) {
88+
$resolved[] = $positional[$index++];
89+
}
6390

64-
if ($typeName !== null && isset($arguments[$argIndex]) && $arguments[$argIndex] instanceof $typeName) {
65-
$resolvedArgs[$paramName] = $arguments[$argIndex++];
91+
break;
92+
}
93+
94+
if (array_key_exists($name, $named)) {
95+
$resolved[] = $named[$name];
96+
97+
continue;
98+
}
99+
100+
$type = self::typeName($param);
101+
102+
if ($type !== null && isset($positional[$index]) && $positional[$index] instanceof $type) {
103+
$resolved[] = $positional[$index++];
66104

67105
continue;
68106
}
69107

70-
if ($typeName !== null && $this->container->has($typeName)) {
71-
$resolvedArgs[$paramName] = $this->container->get($typeName);
108+
if ($type !== null && $this->container->has($type)) {
109+
$resolved[] = $this->container->get($type);
72110

73111
continue;
74112
}
75113

76-
if ($argIndex < $argCount) {
77-
$resolvedArgs[$paramName] = $arguments[$argIndex++];
114+
if ($index < $count) {
115+
$resolved[] = $positional[$index++];
78116
} elseif ($param->isDefaultValueAvailable()) {
79-
$resolvedArgs[$paramName] = $param->getDefaultValue();
117+
$resolved[] = $param->getDefaultValue();
80118
} else {
81-
$resolvedArgs[$paramName] = null;
119+
$resolved[] = null;
82120
}
83121
}
84122

85-
return $resolvedArgs;
123+
return $resolved;
86124
}
87125

88126
/**
89-
* Resolve parameters from explicit named args + container.
90-
* Named args take precedence over container values.
127+
* Resolve arguments, with named arguments taking precedence over the container.
128+
*
129+
* @deprecated Use {@see resolve()} instead; it now handles named arguments directly.
91130
*
92-
* @param array<string, mixed> $namedArgs
131+
* @param array<int|string, mixed> $arguments
93132
*
94-
* @return array<string, mixed> Resolved arguments keyed by parameter name
133+
* @return list<mixed>
95134
*/
96-
public function resolveNamed(ReflectionFunctionAbstract $reflection, array $namedArgs): array
135+
public function resolveNamed(ReflectionFunctionAbstract $reflection, array $arguments): array
97136
{
98-
$params = $reflection->getParameters();
99-
if ($params === []) {
100-
return [];
101-
}
102-
103-
$resolvedArgs = [];
104-
105-
foreach ($params as $param) {
106-
$paramName = $param->getName();
107-
108-
if (array_key_exists($paramName, $namedArgs)) {
109-
$resolvedArgs[$paramName] = $namedArgs[$paramName];
110-
111-
continue;
112-
}
113-
114-
$typeName = self::typeName($param);
115-
116-
if ($typeName !== null && $this->container->has($typeName)) {
117-
$resolvedArgs[$paramName] = $this->container->get($typeName);
118-
119-
continue;
120-
}
121-
122-
$resolvedArgs[$paramName] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
123-
}
124-
125-
return $resolvedArgs;
137+
return $this->resolve($reflection, $arguments);
126138
}
127139

128140
/** Reflect any callable into its ReflectionFunctionAbstract. */
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: ISC
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Parameter\Test\Fixtures;
12+
13+
final class VariadicConsumer
14+
{
15+
/** @var array<array-key, int> */
16+
public readonly array $numbers;
17+
18+
public function __construct(public readonly SampleService $service, int ...$numbers)
19+
{
20+
$this->numbers = $numbers;
21+
}
22+
}

0 commit comments

Comments
 (0)