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
21 changes: 6 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Respect\Parameter

Resolves function and constructor parameters from PSR-11 containers, by type and by name.
Resolves function and constructor parameters from a PSR-11 container by type.

## Install

Expand All @@ -14,8 +14,8 @@ composer require respect/parameter

For each parameter the resolver tries, in order:

1. Container match by **type** (non-builtin)
2. Container match by **parameter name**
1. Positional argument of matching **type**
2. Container match by **type** (non-builtin)
3. Next **positional argument**
4. **Default value**
5. `null`
Expand Down Expand Up @@ -47,7 +47,7 @@ $args = $resolver->resolveNamed(
$constructor,
['username' => 'admin', 'password' => 'secret'],
);
// Named args take precedence, gaps filled from container
// Named args take precedence, gaps filled from container by name and type
```

### Reflect any callable
Expand All @@ -64,14 +64,6 @@ Resolver::reflectCallable('strlen'); // Function name
Resolver::reflectCallable('DateTime::createFromFormat'); // Static method
```

### Convert positional to named

```php
// function greet(string $name, int $age)
$named = Resolver::toNamedArgs($reflection, ['Alice', 30]);
// ['name' => 'Alice', 'age' => 30]
```

### Check accepted types

```php
Expand All @@ -82,10 +74,9 @@ Resolver::acceptsType($reflection, LoggerInterface::class); // true/false

| Method | Type | Description |
|-----------------------------------------|----------|------------------------------------------------------|
| `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 |
| `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` |
| `toNamedArgs($reflection, $positional)` | static | Positional array to name-keyed map |
| `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type |

## License
Expand Down
75 changes: 17 additions & 58 deletions src/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,15 @@
use function str_contains;

/**
* Resolves function/constructor parameters from PSR-11 containers.
* Resolves function/constructor parameters from a PSR-11 container.
*
* For each parameter, tries by type (non-builtin) then by name against each
* container in order. Falls through to positional arguments, then defaults.
* For each parameter, tries by type (non-builtin) against the container.
* Falls through to positional arguments, then defaults.
*/
final readonly class Resolver
{
/** @var array<int|string, ContainerInterface> */
private array $containers;

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

/**
Expand All @@ -66,16 +61,14 @@ public function resolve(ReflectionFunctionAbstract $reflection, array $arguments
$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[$paramName] = $arguments[$argIndex++];

continue;
}

[$found, $value] = $this->fromContainers($paramName, $typeName);
if ($found) {
$resolvedArgs[$paramName] = $value;
if ($typeName !== null && $this->container->has($typeName)) {
$resolvedArgs[$paramName] = $this->container->get($typeName);

continue;
}
Expand All @@ -93,7 +86,7 @@ public function resolve(ReflectionFunctionAbstract $reflection, array $arguments
}

/**
* Resolve parameters from explicit named args + containers.
* Resolve parameters from explicit named args + container.
* Named args take precedence over container values.
*
* @param array<string, mixed> $namedArgs
Expand All @@ -118,9 +111,10 @@ public function resolveNamed(ReflectionFunctionAbstract $reflection, array $name
continue;
}

[$found, $value] = $this->fromContainers($paramName, self::typeName($param));
if ($found) {
$resolvedArgs[$paramName] = $value;
$typeName = self::typeName($param);

if ($typeName !== null && $this->container->has($typeName)) {
$resolvedArgs[$paramName] = $this->container->get($typeName);

continue;
}
Expand All @@ -131,28 +125,6 @@ public function resolveNamed(ReflectionFunctionAbstract $reflection, array $name
return $resolvedArgs;
}

/**
* Convert positional arguments to a name-keyed map using reflection param names.
*
* @param array<int, mixed> $positional
*
* @return array<string, mixed>
*/
public static function toNamedArgs(ReflectionFunctionAbstract $reflection, array $positional): array
{
$named = [];
foreach ($reflection->getParameters() as $param) {
$position = $param->getPosition();
if (!isset($positional[$position])) {
continue;
}

$named[$param->getName()] = $positional[$position];
}

return $named;
}

/** Reflect any callable into its ReflectionFunctionAbstract. */
public static function reflectCallable(callable $callable): ReflectionFunctionAbstract
{
Expand All @@ -161,9 +133,9 @@ public static function reflectCallable(callable $callable): ReflectionFunctionAb
}

if (is_array($callable)) {
/** @var array{object|string, string} $callable */ // phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable
/** @var array{object|class-string, string} $callable */ // phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable

return new ReflectionMethod($callable[0], $callable[1]);
return new ReflectionMethod(...$callable);
}

if (is_object($callable)) {
Expand All @@ -179,7 +151,7 @@ public static function reflectCallable(callable $callable): ReflectionFunctionAb
return new ReflectionFunction($callable);
}

/** Check if any parameter of the function accepts a given type. */
/** @param class-string $type */
public static function acceptsType(ReflectionFunctionAbstract $reflection, string $type): bool
{
foreach ($reflection->getParameters() as $param) {
Expand All @@ -193,26 +165,13 @@ public static function acceptsType(ReflectionFunctionAbstract $reflection, strin
return false;
}

/** @return class-string|null */
private static function typeName(ReflectionParameter $param): string|null
{
$type = $param->getType();

/** @phpstan-ignore return.type */
return $type instanceof ReflectionNamedType && !$type->isBuiltin() ? $type->getName() : null;
}

/** @return array{bool, mixed} */
private function fromContainers(string $paramName, string|null $typeName): array
{
foreach ($this->containers as $container) {
if ($typeName !== null && $container->has($typeName)) {
return [true, $container->get($typeName)];
}

if ($container->has($paramName)) {
return [true, $container->get($paramName)];
}
}

return [false, null];
/* Ignore Reason: !isBuiltin() guarantees class-string */
}
}
21 changes: 0 additions & 21 deletions tests/fixtures/NamedConsumer.php

This file was deleted.

75 changes: 2 additions & 73 deletions tests/unit/ResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
use ReflectionMethod;
use Respect\Parameter\Resolver;
use Respect\Parameter\Test\Fixtures\ArrayContainer;
use Respect\Parameter\Test\Fixtures\NamedConsumer;
use Respect\Parameter\Test\Fixtures\SampleService;
use Respect\Parameter\Test\Fixtures\ServiceConsumer;

Expand All @@ -38,38 +37,6 @@ public function itShouldResolveByType(): void
self::assertSame(42, $args['number']);
}

#[Test]
public function itShouldResolveByName(): void
{
$resolver = new Resolver(new ArrayContainer([
'username' => 'admin',
'password' => 'secret',
]));

$args = $resolver->resolve($this->constructorOf(NamedConsumer::class), []);

self::assertSame('admin', $args['username']);
self::assertSame('secret', $args['password']);
self::assertSame(3306, $args['port']);
}

#[Test]
public function itShouldTryMultipleContainers(): void
{
$service = new SampleService();

$resolver = new Resolver(
new ArrayContainer(['value' => 'named']),
new ArrayContainer([SampleService::class => $service]),
);

$args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), []);

self::assertSame($service, $args['service']);
self::assertSame('named', $args['value']);
self::assertSame(42, $args['number']);
}

#[Test]
public function itShouldAllowUserOverride(): void
{
Expand All @@ -94,7 +61,7 @@ public function itShouldFallThroughToPositionalArgs(): void
}

#[Test]
public function itShouldReturnEmptyWhenNoParams(): void
public function itShouldPassThroughWhenNoParams(): void
{
$resolver = new Resolver(new ArrayContainer());
$fn = new ReflectionFunction(static function (): void {
Expand All @@ -105,26 +72,6 @@ public function itShouldReturnEmptyWhenNoParams(): void
self::assertSame(['a', 'b'], $args);
}

#[Test]
public function itShouldConvertPositionalToNamed(): void
{
$constructor = $this->constructorOf(NamedConsumer::class);

$named = Resolver::toNamedArgs($constructor, ['admin', 'secret', 3306]);

self::assertSame(['username' => 'admin', 'password' => 'secret', 'port' => 3306], $named);
}

#[Test]
public function itShouldConvertPartialPositionalToNamed(): void
{
$constructor = $this->constructorOf(NamedConsumer::class);

$named = Resolver::toNamedArgs($constructor, ['admin']);

self::assertSame(['username' => 'admin'], $named);
}

#[Test]
public function itShouldDetectAcceptedType(): void
{
Expand Down Expand Up @@ -195,10 +142,7 @@ public function itShouldReflectStaticMethodString(): void
public function itShouldResolveNamedArgsWithPrecedenceOverContainer(): void
{
$service = new SampleService();
$resolver = new Resolver(new ArrayContainer([
SampleService::class => $service,
'value' => 'from-container',
]));
$resolver = new Resolver(new ArrayContainer([SampleService::class => $service]));

$args = $resolver->resolveNamed(
$this->constructorOf(ServiceConsumer::class),
Expand All @@ -210,21 +154,6 @@ public function itShouldResolveNamedArgsWithPrecedenceOverContainer(): void
self::assertSame(42, $args['number']);
}

#[Test]
public function itShouldResolveNamedArgsFillingGapsFromContainer(): void
{
$resolver = new Resolver(new ArrayContainer(['password' => 'auto-secret']));

$args = $resolver->resolveNamed(
$this->constructorOf(NamedConsumer::class),
['username' => 'admin'],
);

self::assertSame('admin', $args['username']);
self::assertSame('auto-secret', $args['password']);
self::assertSame(3306, $args['port']);
}

#[Test]
public function itShouldResolveNamedArgsWithEmptyNamedArray(): void
{
Expand Down
Loading