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
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string, mixed>` keyed by parameter name |
| `resolveNamed($reflection, $named)` | instance | Resolve from named args (priority) + containers. Returns `array<string, mixed>` 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 |
Expand Down
29 changes: 15 additions & 14 deletions src/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,20 +35,21 @@
*/
final readonly class Resolver
{
/** @var array<int, ContainerInterface> */
/** @var array<int|string, ContainerInterface> */
private array $containers;

/** @param array<int|string, ContainerInterface> ...$containers */
public function __construct(ContainerInterface ...$containers)
{
$this->containers = array_values($containers);
$this->containers = $containers;
}

/**
* Resolve parameters for a function/constructor from positional arguments.
*
* @param array<int, mixed> $arguments User-provided positional arguments
*
* @return array<int, mixed> Resolved arguments
* @return array<int, mixed>|array<string, mixed> Resolved arguments keyed by parameter name
*/
public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array
{
Expand All @@ -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;
}
}

Expand All @@ -97,7 +98,7 @@ public function resolve(ReflectionFunctionAbstract $reflection, array $arguments
*
* @param array<string, mixed> $namedArgs
*
* @return array<int, mixed> Resolved arguments
* @return array<string, mixed> Resolved arguments keyed by parameter name
*/
public function resolveNamed(ReflectionFunctionAbstract $reflection, array $namedArgs): array
{
Expand All @@ -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;
Expand Down
44 changes: 22 additions & 22 deletions tests/unit/ResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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 {
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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 */
Expand Down
Loading