diff --git a/README.md b/README.md index cc516a6..e547555 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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` @@ -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 @@ -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 @@ -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` keyed by parameter name | -| `resolveNamed($reflection, $named)` | instance | Resolve from named args (priority) + containers. Returns `array` keyed by parameter name | +| `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` | -| `toNamedArgs($reflection, $positional)` | static | Positional array to name-keyed map | | `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type | ## License diff --git a/src/Resolver.php b/src/Resolver.php index 25af17e..f115be1 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -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 */ - private array $containers; - - /** @param array ...$containers */ - public function __construct(ContainerInterface ...$containers) + public function __construct(private ContainerInterface $container) { - $this->containers = $containers; } /** @@ -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; } @@ -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 $namedArgs @@ -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; } @@ -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 $positional - * - * @return array - */ - 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 { @@ -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)) { @@ -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) { @@ -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 */ } } diff --git a/tests/fixtures/NamedConsumer.php b/tests/fixtures/NamedConsumer.php deleted file mode 100644 index e4b5f40..0000000 --- a/tests/fixtures/NamedConsumer.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Parameter\Test\Fixtures; - -final class NamedConsumer -{ - public function __construct( - public readonly string $username, - public readonly string $password, - public readonly int $port = 3306, - ) { - } -} diff --git a/tests/unit/ResolverTest.php b/tests/unit/ResolverTest.php index 25ae75e..c3e8dd6 100644 --- a/tests/unit/ResolverTest.php +++ b/tests/unit/ResolverTest.php @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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), @@ -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 {