Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 40 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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<string, mixed>` keyed by parameter name |
| `resolveNamed($reflection, $named)` | instance | Resolve from named args (priority) + container. Returns `array<string, mixed>` 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<mixed>`, expanding variadics |
| `reflectCallable($callable)` | static | Any callable to `ReflectionFunctionAbstract` |
| `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type |

`Resolver` implements `ParameterResolver`.

## License

Expand Down
25 changes: 25 additions & 0 deletions src/ParameterResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* SPDX-License-Identifier: ISC
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
*/

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<int|string, mixed> $arguments
*
* @return list<mixed>
*/
public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array;
}
128 changes: 70 additions & 58 deletions src/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, mixed> $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<int, mixed>|array<string, mixed> Resolved arguments keyed by parameter name
* @param array<int|string, mixed> $arguments
*
* @return list<mixed>
*/
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++];
}

Comment thread
alganet marked this conversation as resolved.
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<string, mixed> $namedArgs
* @param array<int|string, mixed> $arguments
*
* @return array<string, mixed> Resolved arguments keyed by parameter name
* @return list<mixed>
*/
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. */
Expand Down
22 changes: 22 additions & 0 deletions tests/fixtures/VariadicConsumer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/*
* SPDX-License-Identifier: ISC
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Parameter\Test\Fixtures;

final class VariadicConsumer
{
/** @var array<array-key, int> */
public readonly array $numbers;

public function __construct(public readonly SampleService $service, int ...$numbers)
{
$this->numbers = $numbers;
}
}
Loading