From eefbc3d43ff9d358126a2637d8370bd49b3b7662 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Thu, 25 Jun 2026 18:48:05 -0300 Subject: [PATCH] Rename ParameterResolver interface to Resolver Swap the names so the contract carries the minimal name and the implementation carries the qualifier describing what it does. The interface is the generic `resolve()` promise; the concrete class is specifically about resolving parameters from a PSR-11 container. ParameterResolver (interface) -> Resolver Resolver (class) -> ContainerResolver This frees the bare `Resolver` name for consumers depending on the abstraction (`Resolver $resolver`) and gives future strategies parallel, self-describing names. ContainerResolver's name now carries strictly more information than the interface's, matching the usual contract-vs- implementation convention. BC break (clean, no aliases): ships as a major version bump. - ParameterResolver is removed; type-hint Resolver instead. - The old Resolver class becomes ContainerResolver; `new Resolver(...)` callers must switch to `new ContainerResolver(...)`. README and tests updated; ResolverTest renamed to ContainerResolverTest. --- README.md | 26 +-- src/ContainerResolver.php | 189 ++++++++++++++++++ src/ParameterResolver.php | 25 --- src/Resolver.php | 170 +--------------- ...lverTest.php => ContainerResolverTest.php} | 42 ++-- 5 files changed, 226 insertions(+), 226 deletions(-) create mode 100644 src/ContainerResolver.php delete mode 100644 src/ParameterResolver.php rename tests/unit/{ResolverTest.php => ContainerResolverTest.php} (77%) diff --git a/README.md b/README.md index 61a557d..35aace8 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ For each parameter the resolver tries, in order: A trailing **variadic** parameter receives a matching named argument (if any) followed by every remaining positional argument. ```php -use Respect\Parameter\Resolver; +use Respect\Parameter\ContainerResolver; function notify(Mailer $mailer, Logger $logger, string $to, string $subject = 'Hi') { // ... } -$resolver = new Resolver($container); +$resolver = new ContainerResolver($container); $args = $resolver->resolve(new ReflectionFunction('notify'), ['bob@example.com']); // [Mailer, Logger, 'bob@example.com', 'Hi'] — ordered, ready to splat ``` @@ -54,15 +54,15 @@ $args = $resolver->resolve($constructor, ['username' => 'admin']); ### Bind to the interface -Type-hint `ParameterResolver` (the `resolve()` contract) rather than the concrete `Resolver` to stay +Type-hint `Resolver` (the `resolve()` contract) rather than the concrete `ContainerResolver` to stay decoupled from the implementation: ```php -use Respect\Parameter\ParameterResolver; +use Respect\Parameter\Resolver; final class Factory { - public function __construct(private ParameterResolver $resolver) + public function __construct(private Resolver $resolver) { } } @@ -73,19 +73,19 @@ final class Factory Convert any callable form into a `ReflectionFunctionAbstract`: ```php -use Respect\Parameter\Resolver; +use Respect\Parameter\ContainerResolver; -Resolver::reflectCallable(fn() => ...); // Closure -Resolver::reflectCallable([$obj, 'method']); // Array callable -Resolver::reflectCallable(new Invocable()); // __invoke object -Resolver::reflectCallable('strlen'); // Function name -Resolver::reflectCallable('DateTime::createFromFormat'); // Static method +ContainerResolver::reflectCallable(fn() => ...); // Closure +ContainerResolver::reflectCallable([$obj, 'method']); // Array callable +ContainerResolver::reflectCallable(new Invocable()); // __invoke object +ContainerResolver::reflectCallable('strlen'); // Function name +ContainerResolver::reflectCallable('DateTime::createFromFormat'); // Static method ``` ### Check accepted types ```php -Resolver::acceptsType($reflection, LoggerInterface::class); // true/false +ContainerResolver::acceptsType($reflection, LoggerInterface::class); // true/false ``` ## API @@ -96,7 +96,7 @@ Resolver::acceptsType($reflection, LoggerInterface::class); // true/false | `reflectCallable($callable)` | static | Any callable to `ReflectionFunctionAbstract` | | `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type | -`Resolver` implements `ParameterResolver`. +`ContainerResolver` implements `Resolver`. ## License diff --git a/src/ContainerResolver.php b/src/ContainerResolver.php new file mode 100644 index 0000000..6fa5cb4 --- /dev/null +++ b/src/ContainerResolver.php @@ -0,0 +1,189 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Parameter; + +use Closure; +use Psr\Container\ContainerInterface; +use ReflectionFunction; +use ReflectionFunctionAbstract; +use ReflectionMethod; +use ReflectionNamedType; +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 the arguments to call a function or constructor with, autowiring any parameter that is + * not supplied from a PSR-11 container by type. + * + * The result is always an ordered list ready to splat (`...$args` / `newInstanceArgs`), with + * variadic parameters expanded. + */ +final readonly class ContainerResolver implements Resolver +{ + public function __construct(private ContainerInterface $container) + { + } + + /** + * Resolve the arguments for a function/constructor. + * + * 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. + * + * @param array $arguments + * + * @return list + */ + public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array + { + $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; + } + } + + $resolved = []; + $index = 0; + $count = count($positional); + + 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++]; + } + + 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 ($type !== null && $this->container->has($type)) { + $resolved[] = $this->container->get($type); + + continue; + } + + if ($index < $count) { + $resolved[] = $positional[$index++]; + } elseif ($param->isDefaultValueAvailable()) { + $resolved[] = $param->getDefaultValue(); + } else { + $resolved[] = null; + } + } + + return $resolved; + } + + /** + * Resolve arguments, with named arguments taking precedence over the container. + * + * @deprecated Use {@see resolve()} instead; it now handles named arguments directly. + * + * @param array $arguments + * + * @return list + */ + public function resolveNamed(ReflectionFunctionAbstract $reflection, array $arguments): array + { + return $this->resolve($reflection, $arguments); + } + + /** Reflect any callable into its ReflectionFunctionAbstract. */ + public static function reflectCallable(callable $callable): ReflectionFunctionAbstract + { + if ($callable instanceof Closure) { + return new ReflectionFunction($callable); + } + + if (is_array($callable)) { + /** @var array{object|class-string, string} $callable */ // phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable + + return new ReflectionMethod(...$callable); + } + + if (is_object($callable)) { + return new ReflectionMethod($callable, '__invoke'); + } + + if (is_string($callable) && str_contains($callable, '::')) { + return ReflectionMethod::createFromMethodName($callable); + } + + assert(is_string($callable)); + + return new ReflectionFunction($callable); + } + + /** @param class-string $type */ + public static function acceptsType(ReflectionFunctionAbstract $reflection, string $type): bool + { + foreach ($reflection->getParameters() as $param) { + $typeName = self::typeName($param); + + if ($typeName !== null && is_a($typeName, $type, true)) { + return true; + } + } + + 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; + /* Ignore Reason: !isBuiltin() guarantees class-string */ + } +} diff --git a/src/ParameterResolver.php b/src/ParameterResolver.php deleted file mode 100644 index e836ac7..0000000 --- a/src/ParameterResolver.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - -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 $arguments - * - * @return list - */ - public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array; -} diff --git a/src/Resolver.php b/src/Resolver.php index 037dabb..125d2fd 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -10,180 +10,16 @@ namespace Respect\Parameter; -use Closure; -use Psr\Container\ContainerInterface; -use ReflectionFunction; use ReflectionFunctionAbstract; -use ReflectionMethod; -use ReflectionNamedType; -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 the arguments to call a function or constructor with, autowiring any parameter that is - * not supplied from a PSR-11 container by type. - * - * The result is always an ordered list ready to splat (`...$args` / `newInstanceArgs`), with - * variadic parameters expanded. - */ -final readonly class Resolver implements ParameterResolver +interface Resolver { - public function __construct(private ContainerInterface $container) - { - } - /** - * Resolve the arguments for a function/constructor. - * - * 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. + * Resolve the arguments for a function/constructor into an ordered, ready-to-splat list. * * @param array $arguments * * @return list */ - public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array - { - $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; - } - } - - $resolved = []; - $index = 0; - $count = count($positional); - - 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++]; - } - - 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 ($type !== null && $this->container->has($type)) { - $resolved[] = $this->container->get($type); - - continue; - } - - if ($index < $count) { - $resolved[] = $positional[$index++]; - } elseif ($param->isDefaultValueAvailable()) { - $resolved[] = $param->getDefaultValue(); - } else { - $resolved[] = null; - } - } - - return $resolved; - } - - /** - * Resolve arguments, with named arguments taking precedence over the container. - * - * @deprecated Use {@see resolve()} instead; it now handles named arguments directly. - * - * @param array $arguments - * - * @return list - */ - public function resolveNamed(ReflectionFunctionAbstract $reflection, array $arguments): array - { - return $this->resolve($reflection, $arguments); - } - - /** Reflect any callable into its ReflectionFunctionAbstract. */ - public static function reflectCallable(callable $callable): ReflectionFunctionAbstract - { - if ($callable instanceof Closure) { - return new ReflectionFunction($callable); - } - - if (is_array($callable)) { - /** @var array{object|class-string, string} $callable */ // phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable - - return new ReflectionMethod(...$callable); - } - - if (is_object($callable)) { - return new ReflectionMethod($callable, '__invoke'); - } - - if (is_string($callable) && str_contains($callable, '::')) { - return ReflectionMethod::createFromMethodName($callable); - } - - assert(is_string($callable)); - - return new ReflectionFunction($callable); - } - - /** @param class-string $type */ - public static function acceptsType(ReflectionFunctionAbstract $reflection, string $type): bool - { - foreach ($reflection->getParameters() as $param) { - $typeName = self::typeName($param); - - if ($typeName !== null && is_a($typeName, $type, true)) { - return true; - } - } - - 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; - /* Ignore Reason: !isBuiltin() guarantees class-string */ - } + public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array; } diff --git a/tests/unit/ResolverTest.php b/tests/unit/ContainerResolverTest.php similarity index 77% rename from tests/unit/ResolverTest.php rename to tests/unit/ContainerResolverTest.php index 736c1e0..d42949c 100644 --- a/tests/unit/ResolverTest.php +++ b/tests/unit/ContainerResolverTest.php @@ -16,27 +16,27 @@ use ReflectionClass; use ReflectionFunction; use ReflectionMethod; -use Respect\Parameter\ParameterResolver; +use Respect\Parameter\ContainerResolver; use Respect\Parameter\Resolver; use Respect\Parameter\Test\Fixtures\ArrayContainer; use Respect\Parameter\Test\Fixtures\SampleService; use Respect\Parameter\Test\Fixtures\ServiceConsumer; use Respect\Parameter\Test\Fixtures\VariadicConsumer; -#[CoversClass(Resolver::class)] -final class ResolverTest extends TestCase +#[CoversClass(ContainerResolver::class)] +final class ContainerResolverTest extends TestCase { #[Test] - public function itShouldImplementParameterResolver(): void + public function itShouldImplementResolver(): void { - self::assertInstanceOf(ParameterResolver::class, new Resolver(new ArrayContainer())); + self::assertInstanceOf(Resolver::class, new ContainerResolver(new ArrayContainer())); } #[Test] public function itShouldResolveByType(): void { $service = new SampleService(); - $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => $service])); $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), ['hello']); @@ -48,7 +48,7 @@ public function itShouldAllowUserOverride(): void { $default = new SampleService(); $explicit = new SampleService(); - $resolver = new Resolver(new ArrayContainer([SampleService::class => $default])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => $default])); $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), [$explicit, 'hello']); @@ -59,7 +59,7 @@ public function itShouldAllowUserOverride(): void #[Test] public function itShouldFallThroughToPositionalArgs(): void { - $resolver = new Resolver(new ArrayContainer()); + $resolver = new ContainerResolver(new ArrayContainer()); $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), ['positional']); @@ -69,7 +69,7 @@ public function itShouldFallThroughToPositionalArgs(): void #[Test] public function itShouldPassThroughWhenNoParams(): void { - $resolver = new Resolver(new ArrayContainer()); + $resolver = new ContainerResolver(new ArrayContainer()); $fn = new ReflectionFunction(static function (): void { }); @@ -83,8 +83,8 @@ public function itShouldDetectAcceptedType(): void { $constructor = $this->constructorOf(ServiceConsumer::class); - self::assertTrue(Resolver::acceptsType($constructor, SampleService::class)); - self::assertFalse(Resolver::acceptsType($constructor, ArrayContainer::class)); + self::assertTrue(ContainerResolver::acceptsType($constructor, SampleService::class)); + self::assertFalse(ContainerResolver::acceptsType($constructor, ArrayContainer::class)); } #[Test] @@ -94,7 +94,7 @@ public function itShouldReflectClosure(): void return $a; }; - $reflection = Resolver::reflectCallable($fn); + $reflection = ContainerResolver::reflectCallable($fn); self::assertInstanceOf(ReflectionFunction::class, $reflection); self::assertSame('a', $reflection->getParameters()[0]->getName()); @@ -103,7 +103,7 @@ public function itShouldReflectClosure(): void #[Test] public function itShouldReflectArrayCallable(): void { - $reflection = Resolver::reflectCallable([new ArrayContainer([]), 'has']); + $reflection = ContainerResolver::reflectCallable([new ArrayContainer([]), 'has']); self::assertInstanceOf(ReflectionMethod::class, $reflection); self::assertSame('has', $reflection->getName()); @@ -119,7 +119,7 @@ public function __invoke(int $x): int } }; - $reflection = Resolver::reflectCallable($invocable); + $reflection = ContainerResolver::reflectCallable($invocable); self::assertInstanceOf(ReflectionMethod::class, $reflection); self::assertSame('__invoke', $reflection->getName()); @@ -129,7 +129,7 @@ public function __invoke(int $x): int #[Test] public function itShouldReflectNamedFunction(): void { - $reflection = Resolver::reflectCallable('strlen'); + $reflection = ContainerResolver::reflectCallable('strlen'); self::assertInstanceOf(ReflectionFunction::class, $reflection); self::assertSame('strlen', $reflection->getName()); @@ -138,7 +138,7 @@ public function itShouldReflectNamedFunction(): void #[Test] public function itShouldReflectStaticMethodString(): void { - $reflection = Resolver::reflectCallable('DateTime::createFromFormat'); + $reflection = ContainerResolver::reflectCallable('DateTime::createFromFormat'); self::assertInstanceOf(ReflectionMethod::class, $reflection); self::assertSame('createFromFormat', $reflection->getName()); @@ -147,7 +147,7 @@ public function itShouldReflectStaticMethodString(): void #[Test] public function itShouldKeepDeprecatedResolveNamedAsAnAliasOfResolve(): void { - $resolver = new Resolver(new ArrayContainer([SampleService::class => new SampleService()])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => new SampleService()])); $constructor = $this->constructorOf(ServiceConsumer::class); self::assertSame( @@ -160,7 +160,7 @@ public function itShouldKeepDeprecatedResolveNamedAsAnAliasOfResolve(): void public function itShouldExpandVariadicArguments(): void { $service = new SampleService(); - $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => $service])); $args = $resolver->resolve($this->constructorOf(VariadicConsumer::class), [1, 2, 3]); @@ -171,7 +171,7 @@ public function itShouldExpandVariadicArguments(): void public function itShouldSupplyVariadicElementByName(): void { $service = new SampleService(); - $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => $service])); $args = $resolver->resolve($this->constructorOf(VariadicConsumer::class), ['numbers' => 9]); $consumer = (new ReflectionClass(VariadicConsumer::class))->newInstanceArgs($args); @@ -184,7 +184,7 @@ public function itShouldSupplyVariadicElementByName(): void public function itShouldResolveArgumentsReadyToSplatIntoVariadicConstructor(): void { $service = new SampleService(); - $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => $service])); $args = $resolver->resolve($this->constructorOf(VariadicConsumer::class), [1, 2, 3]); $consumer = (new ReflectionClass(VariadicConsumer::class))->newInstanceArgs($args); @@ -197,7 +197,7 @@ public function itShouldResolveArgumentsReadyToSplatIntoVariadicConstructor(): v public function itShouldResolveNamedArgumentsReadyToSplat(): void { $service = new SampleService(); - $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => $service])); $args = $resolver->resolve($this->constructorOf(ServiceConsumer::class), ['value' => 'hi', 'number' => 7]); $consumer = (new ReflectionClass(ServiceConsumer::class))->newInstanceArgs($args);