diff --git a/README.md b/README.md index e547555..826cf46 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,17 @@ composer require respect/parameter ## Usage +The package offers two contracts with different guarantees: + +- `Resolver` **completes a call**: it returns a full argument list keyed by + parameter name, padding gaps with container services, defaults, or `null`. + Implemented by `ContainerResolver`. +- `Augmenter` **assists a factory**: the given arguments stay authoritative — + never rebound, reordered, or padded — and the container only fills genuine + gaps. Implemented by `ContainerAugmenter`. + +Type-hint the interfaces to keep implementations swappable and testable. + ### Resolve from a container For each parameter the resolver tries, in order: @@ -21,13 +32,13 @@ For each parameter the resolver tries, in order: 5. `null` ```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' => Mailer, 'logger' => Logger, 'to' => 'bob@example.com', 'subject' => 'Hi'] ``` @@ -50,34 +61,82 @@ $args = $resolver->resolveNamed( // Named args take precedence, gaps filled from container by name and type ``` +### Augment arguments + +Use the augmenter when the arguments must stay exactly as the caller provided +them (e.g. factories that pass user input straight to a constructor) and the +container should only supply the missing services: + +```php +use Respect\Parameter\ContainerAugmenter; + +final class Notifier +{ + public function __construct( + private string $channel, + private Mailer|null $mailer = null, + ) { + } +} + +$augmenter = new ContainerAugmenter($container); +$args = $augmenter->augment($constructor, ['slack']); +// ['slack', 'mailer' => Mailer] — positional args untouched, gaps named +``` + +Variadic, builtin-typed, and already-filled parameters are never augmented. +Extra arguments (e.g. for variadic parameters) pass through unchanged, and +missing arguments are never padded with defaults or `null`. + +#### Unresolvable types + +Value-like classes should never be served by the container, even when it can +provide them — a container-cached `DateTimeImmutable` is a frozen clock. +List them at construction to exclude them from container lookups: + +```php +$augmenter = new ContainerAugmenter($container, [ + DateTimeImmutable::class, + DateTimeInterface::class, +]); +``` + ### Reflect any callable Convert any callable form into a `ReflectionFunctionAbstract`: ```php -use Respect\Parameter\Resolver; +use Respect\Parameter\Reflector; -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 +Reflector::reflectCallable(fn() => ...); // Closure +Reflector::reflectCallable([$obj, 'method']); // Array callable +Reflector::reflectCallable(new Invocable()); // __invoke object +Reflector::reflectCallable('strlen'); // Function name +Reflector::reflectCallable('DateTime::createFromFormat'); // Static method ``` ### Check accepted types ```php -Resolver::acceptsType($reflection, LoggerInterface::class); // true/false +Reflector::acceptsType($reflection, LoggerInterface::class); // true/false ``` ## API -| Method | Type | Description | -|-----------------------------------------|----------|------------------------------------------------------| -| `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` | -| `acceptsType($reflection, $type)` | static | Check if any parameter accepts a type | +| Method | Defined on | Description | +|---------------------------------------------|-------------|------------------------------------------------------| +| `resolve($reflection, $positional)` | `Resolver` | Resolve parameters from positional args + container. Returns `array` keyed by parameter name | +| `resolveNamed($reflection, $named)` | `Resolver` | Resolve from named args (priority) + container. Returns `array` keyed by parameter name | +| `augment($reflection, $args)` | `Augmenter` | Fill only unfilled parameters from the container; given args are never rebound, reordered, or padded | +| `Reflector::reflectCallable($callable)` | `Reflector` | Any callable to `ReflectionFunctionAbstract` | +| `Reflector::acceptsType($reflection, $type)`| `Reflector` | Check if any parameter accepts a type | + +## Upgrading from 1.x + +- `Resolver` is now an interface; the concrete class is `ContainerResolver`. +- `Resolver::reflectCallable()` and `Resolver::acceptsType()` moved to `Reflector`. +- The new `Augmenter`/`ContainerAugmenter` fill unfilled parameters without + touching the given arguments. ## License diff --git a/composer.json b/composer.json index 158654a..b36a000 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,10 @@ "psr-4": { "Respect\\Parameter\\Test\\": "tests/", "Respect\\Parameter\\Test\\Fixtures\\": "tests/fixtures" - } + }, + "files": [ + "tests/fixtures/functions.php" + ] }, "scripts": { "phpcs": "vendor/bin/phpcs", @@ -43,6 +46,11 @@ "@phpunit" ] }, + "extra": { + "branch-alias": { + "dev-main": "2.0.x-dev" + } + }, "config": { "sort-packages": true, "allow-plugins": { diff --git a/src/Augmenter.php b/src/Augmenter.php new file mode 100644 index 0000000..5f8008c --- /dev/null +++ b/src/Augmenter.php @@ -0,0 +1,30 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Parameter; + +use ReflectionFunctionAbstract; + +interface Augmenter +{ + /** + * Augment the given arguments with values for the parameters they do not already fill. + * + * The given arguments are authoritative: they are never rebound, reordered, + * or padded with defaults or null. Only parameters left unfilled may gain a + * value, added as named arguments. Variadic and builtin-typed parameters + * are never augmented. + * + * @param array $arguments Positional and/or named arguments + * + * @return array + */ + public function augment(ReflectionFunctionAbstract $reflection, array $arguments): array; +} diff --git a/src/ContainerAugmenter.php b/src/ContainerAugmenter.php new file mode 100644 index 0000000..3fd7e8a --- /dev/null +++ b/src/ContainerAugmenter.php @@ -0,0 +1,126 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Parameter; + +use Psr\Container\ContainerInterface; +use ReflectionFunctionAbstract; +use ReflectionMethod; +use ReflectionNamedType; + +use function array_filter; +use function array_is_list; +use function array_key_exists; +use function array_keys; +use function class_exists; +use function count; +use function in_array; +use function interface_exists; +use function is_int; + +/** + * Augments arguments with services from a PSR-11 container. + * + * Types listed as unresolvable are never looked up in the container, which + * keeps value-like classes (clocks, dates) from being served as services. + */ +final class ContainerAugmenter implements Augmenter +{ + /** @var array> */ + private array $augmentableParametersCache = []; + + /** @param array $unresolvableTypes */ + public function __construct( + private readonly ContainerInterface $container, + private readonly array $unresolvableTypes = [], + ) { + } + + /** + * @param array $arguments Positional and/or named arguments + * + * @return array + */ + public function augment(ReflectionFunctionAbstract $reflection, array $arguments): array + { + if (count($arguments) >= $reflection->getNumberOfParameters()) { + return $arguments; + } + + $augmentableParameters = $this->augmentableParameters($reflection); + if ($augmentableParameters === []) { + return $arguments; + } + + $positionalArgumentsCount = count( + array_is_list($arguments) ? $arguments : array_filter(array_keys($arguments), is_int(...)), + ); + + foreach ($augmentableParameters as [$position, $name, $type]) { + if ($position < $positionalArgumentsCount || array_key_exists($name, $arguments)) { + continue; + } + + if (!$this->container->has($type)) { + continue; + } + + $arguments[$name] = $this->container->get($type); + } + + return $arguments; + } + + /** @return list */ + private function augmentableParameters(ReflectionFunctionAbstract $reflection): array + { + $cacheKey = self::createCacheKey($reflection); + if (isset($this->augmentableParametersCache[$cacheKey])) { + return $this->augmentableParametersCache[$cacheKey]; + } + + $parameters = []; + foreach ($reflection->getParameters() as $parameter) { + $type = $parameter->getType(); + if ($parameter->isVariadic() || !$type instanceof ReflectionNamedType || $type->isBuiltin()) { + continue; + } + + $typeName = $type->getName(); + if (!class_exists($typeName) && !interface_exists($typeName)) { + continue; + } + + if (in_array($typeName, $this->unresolvableTypes, true)) { + continue; + } + + $parameters[] = [$parameter->getPosition(), $parameter->getName(), $typeName]; + } + + return $this->augmentableParametersCache[$cacheKey] = $parameters; + } + + private static function createCacheKey(ReflectionFunctionAbstract $reflection): string + { + if ($reflection instanceof ReflectionMethod) { + return $reflection->class . '::' . $reflection->name; + } + + if (!$reflection->isClosure()) { + return $reflection->name; + } + + $file = $reflection->getFileName() ?: 'internal'; + $line = $reflection->getStartLine() ?: 0; + + return $reflection->getName() . '@' . $file . ':' . $line; + } +} diff --git a/src/ContainerResolver.php b/src/ContainerResolver.php new file mode 100644 index 0000000..e0ec976 --- /dev/null +++ b/src/ContainerResolver.php @@ -0,0 +1,124 @@ + + * SPDX-FileContributor: Henrique Moody + */ + +declare(strict_types=1); + +namespace Respect\Parameter; + +use Psr\Container\ContainerInterface; +use ReflectionFunctionAbstract; +use ReflectionNamedType; +use ReflectionParameter; + +use function array_key_exists; +use function count; + +/** + * Resolves function/constructor parameters from a PSR-11 container. + * + * For each parameter, tries by type (non-builtin) against the container. + * Falls through to positional arguments, then defaults. + */ +final readonly class ContainerResolver implements Resolver +{ + public function __construct(private ContainerInterface $container) + { + } + + /** + * @param array $arguments User-provided positional arguments + * + * @return array|array Resolved arguments keyed by parameter name + */ + public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array + { + $params = $reflection->getParameters(); + if ($params === []) { + return $arguments; + } + + $resolvedArgs = []; + $argIndex = 0; + $argCount = count($arguments); + + foreach ($params as $param) { + $paramName = $param->getName(); + $typeName = self::typeName($param); + + if ($typeName !== null && isset($arguments[$argIndex]) && $arguments[$argIndex] instanceof $typeName) { + $resolvedArgs[$paramName] = $arguments[$argIndex++]; + + continue; + } + + if ($typeName !== null && $this->container->has($typeName)) { + $resolvedArgs[$paramName] = $this->container->get($typeName); + + continue; + } + + if ($argIndex < $argCount) { + $resolvedArgs[$paramName] = $arguments[$argIndex++]; + } elseif ($param->isDefaultValueAvailable()) { + $resolvedArgs[$paramName] = $param->getDefaultValue(); + } else { + $resolvedArgs[$paramName] = null; + } + } + + return $resolvedArgs; + } + + /** + * @param array $namedArgs + * + * @return array Resolved arguments keyed by parameter name + */ + public function resolveNamed(ReflectionFunctionAbstract $reflection, array $namedArgs): 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 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/Reflector.php b/src/Reflector.php new file mode 100644 index 0000000..90df693 --- /dev/null +++ b/src/Reflector.php @@ -0,0 +1,71 @@ + + * SPDX-FileContributor: Henrique Moody + */ + +declare(strict_types=1); + +namespace Respect\Parameter; + +use Closure; +use ReflectionFunction; +use ReflectionFunctionAbstract; +use ReflectionMethod; +use ReflectionNamedType; + +use function assert; +use function is_a; +use function is_array; +use function is_object; +use function is_string; +use function str_contains; + +final class Reflector +{ + /** 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) { + $paramType = $param->getType(); + if (!$paramType instanceof ReflectionNamedType || $paramType->isBuiltin()) { + continue; + } + + if (is_a($paramType->getName(), $type, true)) { + return true; + } + } + + return false; + } +} diff --git a/src/Resolver.php b/src/Resolver.php index f115be1..1a5e1e9 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -4,174 +4,38 @@ * SPDX-License-Identifier: ISC * SPDX-FileCopyrightText: (c) Respect Project Contributors * SPDX-FileContributor: Alexandre Gomes Gaigalas + * SPDX-FileContributor: Henrique Moody */ 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 assert; -use function count; -use function is_a; -use function is_array; -use function is_object; -use function is_string; -use function str_contains; - -/** - * Resolves function/constructor parameters from a PSR-11 container. - * - * For each parameter, tries by type (non-builtin) against the container. - * Falls through to positional arguments, then defaults. - */ -final readonly class Resolver +interface Resolver { - public function __construct(private ContainerInterface $container) - { - } - /** * Resolve parameters for a function/constructor from positional arguments. * + * For each parameter, tries in order: positional argument of matching type, + * container match by type, next positional argument, default value, null. + * * @param array $arguments User-provided positional arguments * * @return array|array Resolved arguments keyed by parameter name */ - public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array - { - $params = $reflection->getParameters(); - if ($params === []) { - return $arguments; - } - - $resolvedArgs = []; - $argIndex = 0; - $argCount = count($arguments); - - foreach ($params as $param) { - $paramName = $param->getName(); - $typeName = self::typeName($param); - - if ($typeName !== null && isset($arguments[$argIndex]) && $arguments[$argIndex] instanceof $typeName) { - $resolvedArgs[$paramName] = $arguments[$argIndex++]; - - continue; - } - - if ($typeName !== null && $this->container->has($typeName)) { - $resolvedArgs[$paramName] = $this->container->get($typeName); - - continue; - } - - if ($argIndex < $argCount) { - $resolvedArgs[$paramName] = $arguments[$argIndex++]; - } elseif ($param->isDefaultValueAvailable()) { - $resolvedArgs[$paramName] = $param->getDefaultValue(); - } else { - $resolvedArgs[$paramName] = null; - } - } - - return $resolvedArgs; - } + public function resolve(ReflectionFunctionAbstract $reflection, array $arguments): array; /** - * Resolve parameters from explicit named args + container. - * Named args take precedence over container values. + * Resolve parameters from explicit named arguments. + * + * Named arguments take precedence, gaps are filled from the container by + * type, then by default value, then null. * * @param array $namedArgs * * @return array Resolved arguments keyed by parameter name */ - public function resolveNamed(ReflectionFunctionAbstract $reflection, array $namedArgs): 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; - } - - /** 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 resolveNamed(ReflectionFunctionAbstract $reflection, array $namedArgs): array; } diff --git a/tests/fixtures/OptionalServiceConsumer.php b/tests/fixtures/OptionalServiceConsumer.php new file mode 100644 index 0000000..9703f72 --- /dev/null +++ b/tests/fixtures/OptionalServiceConsumer.php @@ -0,0 +1,20 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Parameter\Test\Fixtures; + +final class OptionalServiceConsumer +{ + public function __construct( + public readonly string $name = 'default', + public readonly SampleService|null $service = null, + ) { + } +} diff --git a/tests/fixtures/functions.php b/tests/fixtures/functions.php new file mode 100644 index 0000000..2b11d7b --- /dev/null +++ b/tests/fixtures/functions.php @@ -0,0 +1,24 @@ + + */ + +declare(strict_types=1); + +// phpcs:disable Squiz.Functions.GlobalFunction.Found + +namespace Respect\Parameter\Test\Fixtures; + +function namedFunctionWithService(string $name, SampleService $service): bool +{ + return true; +} + +// phpcs:ignore SlevomatCodingStandard.PHP.RequireExplicitAssertion.RequiredExplicitAssertion +function functionWithNonExistentType(NonExistentClass123 $x): bool // @phpstan-ignore class.notFound +{ + return true; +} diff --git a/tests/unit/ContainerAugmenterTest.php b/tests/unit/ContainerAugmenterTest.php new file mode 100644 index 0000000..0bd4c8a --- /dev/null +++ b/tests/unit/ContainerAugmenterTest.php @@ -0,0 +1,214 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Parameter\Test\Unit; + +use DateTimeImmutable; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use Respect\Parameter\ContainerAugmenter; +use Respect\Parameter\Test\Fixtures\ArrayContainer; +use Respect\Parameter\Test\Fixtures\OptionalServiceConsumer; +use Respect\Parameter\Test\Fixtures\SampleService; + +use function Respect\Parameter\Test\Fixtures\namedFunctionWithService; + +#[CoversClass(ContainerAugmenter::class)] +final class ContainerAugmenterTest extends TestCase +{ + #[Test] + public function itShouldAugmentArgumentsWithContainerValuesForUnfilledParameters(): void + { + $service = new SampleService(); + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => $service])); + + self::assertSame( + ['some name', 'service' => $service], + $augmenter->augment($this->constructorOf(OptionalServiceConsumer::class), ['some name']), + ); + } + + #[Test] + public function itShouldNotAugmentWhenPositionalArgumentsFillAllParameters(): void + { + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => new SampleService()])); + $arguments = ['some name', new SampleService()]; + + self::assertSame( + $arguments, + $augmenter->augment($this->constructorOf(OptionalServiceConsumer::class), $arguments), + ); + } + + #[Test] + public function itShouldNotAugmentWhenNamedArgumentsFillAugmentableParameters(): void + { + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => new SampleService()])); + $arguments = ['service' => new SampleService()]; + + self::assertSame( + $arguments, + $augmenter->augment($this->constructorOf(OptionalServiceConsumer::class), $arguments), + ); + } + + #[Test] + public function itShouldNotAugmentWhenContainerDoesNotHaveParameterType(): void + { + $augmenter = new ContainerAugmenter(new ArrayContainer()); + + self::assertSame([], $augmenter->augment($this->constructorOf(OptionalServiceConsumer::class), [])); + } + + #[Test] + public function itShouldNotAugmentUnresolvableTypes(): void + { + $service = new SampleService(); + $augmenter = new ContainerAugmenter( + new ArrayContainer([SampleService::class => $service]), + [SampleService::class], + ); + + self::assertSame( + ['some name'], + $augmenter->augment($this->constructorOf(OptionalServiceConsumer::class), ['some name']), + ); + } + + #[Test] + public function itShouldNotAugmentVariadicParameters(): void + { + $service = new SampleService(); + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => $service])); + + $closure = static fn(string $name, SampleService ...$services): bool => true; + + self::assertSame([], $augmenter->augment(new ReflectionFunction($closure), [])); + } + + #[Test] + public function itShouldNotAugmentBuiltinTypes(): void + { + $augmenter = new ContainerAugmenter(new ArrayContainer(['string' => 'value'])); + + $closure = static fn(string $name, int $count): bool => true; + + self::assertSame([], $augmenter->augment(new ReflectionFunction($closure), [])); + } + + #[Test] + public function itShouldNotAugmentParametersWithNonExistentType(): void + { + $augmenter = new ContainerAugmenter(new ArrayContainer()); + + $function = new ReflectionFunction('Respect\Parameter\Test\Fixtures\functionWithNonExistentType'); + + self::assertSame([], $augmenter->augment($function, [])); + } + + #[Test] + public function itShouldAugmentArgumentsForClosure(): void + { + $service = new SampleService(); + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => $service])); + + $closure = static fn(string $name, SampleService $service): bool => true; + + self::assertSame( + ['some name', 'service' => $service], + $augmenter->augment(new ReflectionFunction($closure), ['some name']), + ); + } + + #[Test] + public function itShouldKeepNamedArgumentsWhenAugmenting(): void + { + $service = new SampleService(); + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => $service])); + + $closure = static fn(string $name, SampleService $service): bool => true; + + self::assertSame( + ['name' => 'some name', 'service' => $service], + $augmenter->augment(new ReflectionFunction($closure), ['name' => 'some name']), + ); + } + + #[Test] + public function itShouldAugmentArgumentsForNamedFunction(): void + { + $service = new SampleService(); + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => $service])); + + $function = new ReflectionFunction(namedFunctionWithService(...)); + + self::assertSame( + ['some name', 'service' => $service], + $augmenter->augment($function, ['some name']), + ); + } + + #[Test] + public function itShouldCreateCacheKeyForNamedFunction(): void + { + $service = new SampleService(); + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => $service])); + + $function = new ReflectionFunction('Respect\Parameter\Test\Fixtures\namedFunctionWithService'); + + self::assertSame( + ['some name', 'service' => $service], + $augmenter->augment($function, ['some name']), + ); + } + + #[Test] + public function itShouldUseCachedAugmentableParametersOnSubsequentCalls(): void + { + $service = new SampleService(); + $augmenter = new ContainerAugmenter(new ArrayContainer([SampleService::class => $service])); + + $constructor = $this->constructorOf(OptionalServiceConsumer::class); + + $augmenter->augment($constructor, ['some name']); + + self::assertSame( + ['another name', 'service' => $service], + $augmenter->augment($constructor, ['another name']), + ); + } + + #[Test] + public function itShouldNotAugmentDateTimeTypesWhenListedAsUnresolvable(): void + { + $now = new DateTimeImmutable(); + $augmenter = new ContainerAugmenter( + new ArrayContainer([DateTimeImmutable::class => $now]), + [DateTimeImmutable::class], + ); + + $closure = static fn(DateTimeImmutable $date): bool => true; + + self::assertSame([], $augmenter->augment(new ReflectionFunction($closure), [])); + } + + /** @param class-string $class */ + private function constructorOf(string $class): ReflectionMethod + { + $constructor = (new ReflectionClass($class))->getConstructor(); + self::assertNotNull($constructor); + + return $constructor; + } +} diff --git a/tests/unit/ResolverTest.php b/tests/unit/ContainerResolverTest.php similarity index 52% rename from tests/unit/ResolverTest.php rename to tests/unit/ContainerResolverTest.php index c3e8dd6..fad87b4 100644 --- a/tests/unit/ResolverTest.php +++ b/tests/unit/ContainerResolverTest.php @@ -4,6 +4,7 @@ * SPDX-License-Identifier: ISC * SPDX-FileCopyrightText: (c) Respect Project Contributors * SPDX-FileContributor: Alexandre Gomes Gaigalas + * SPDX-FileContributor: Henrique Moody */ declare(strict_types=1); @@ -16,19 +17,19 @@ use ReflectionClass; use ReflectionFunction; use ReflectionMethod; -use Respect\Parameter\Resolver; +use Respect\Parameter\ContainerResolver; use Respect\Parameter\Test\Fixtures\ArrayContainer; use Respect\Parameter\Test\Fixtures\SampleService; use Respect\Parameter\Test\Fixtures\ServiceConsumer; -#[CoversClass(Resolver::class)] -final class ResolverTest extends TestCase +#[CoversClass(ContainerResolver::class)] +final class ContainerResolverTest extends TestCase { #[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']); @@ -42,7 +43,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']); @@ -53,7 +54,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']); @@ -63,7 +64,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 { }); @@ -72,77 +73,11 @@ public function itShouldPassThroughWhenNoParams(): void self::assertSame(['a', 'b'], $args); } - #[Test] - public function itShouldDetectAcceptedType(): void - { - $constructor = $this->constructorOf(ServiceConsumer::class); - - self::assertTrue(Resolver::acceptsType($constructor, SampleService::class)); - self::assertFalse(Resolver::acceptsType($constructor, ArrayContainer::class)); - } - - #[Test] - public function itShouldReflectClosure(): void - { - $fn = static function (string $a): string { - return $a; - }; - - $reflection = Resolver::reflectCallable($fn); - - self::assertInstanceOf(ReflectionFunction::class, $reflection); - self::assertSame('a', $reflection->getParameters()[0]->getName()); - } - - #[Test] - public function itShouldReflectArrayCallable(): void - { - $reflection = Resolver::reflectCallable([new ArrayContainer([]), 'has']); - - self::assertInstanceOf(ReflectionMethod::class, $reflection); - self::assertSame('has', $reflection->getName()); - } - - #[Test] - public function itShouldReflectInvocableObject(): void - { - $invocable = new class () { - public function __invoke(int $x): int - { - return $x; - } - }; - - $reflection = Resolver::reflectCallable($invocable); - - self::assertInstanceOf(ReflectionMethod::class, $reflection); - self::assertSame('__invoke', $reflection->getName()); - self::assertSame('x', $reflection->getParameters()[0]->getName()); - } - - #[Test] - public function itShouldReflectNamedFunction(): void - { - $reflection = Resolver::reflectCallable('strlen'); - - self::assertInstanceOf(ReflectionFunction::class, $reflection); - self::assertSame('strlen', $reflection->getName()); - } - - #[Test] - public function itShouldReflectStaticMethodString(): void - { - $reflection = Resolver::reflectCallable('DateTime::createFromFormat'); - - self::assertInstanceOf(ReflectionMethod::class, $reflection); - self::assertSame('createFromFormat', $reflection->getName()); - } - #[Test] public function itShouldResolveNamedArgsWithPrecedenceOverContainer(): void { $service = new SampleService(); - $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => $service])); $args = $resolver->resolveNamed( $this->constructorOf(ServiceConsumer::class), @@ -158,7 +93,7 @@ public function itShouldResolveNamedArgsWithPrecedenceOverContainer(): void public function itShouldResolveNamedArgsWithEmptyNamedArray(): void { $service = new SampleService(); - $resolver = new Resolver(new ArrayContainer([SampleService::class => $service])); + $resolver = new ContainerResolver(new ArrayContainer([SampleService::class => $service])); $args = $resolver->resolveNamed( $this->constructorOf(ServiceConsumer::class), diff --git a/tests/unit/ReflectorTest.php b/tests/unit/ReflectorTest.php new file mode 100644 index 0000000..af1ab74 --- /dev/null +++ b/tests/unit/ReflectorTest.php @@ -0,0 +1,94 @@ + + * SPDX-FileContributor: Henrique Moody + */ + +declare(strict_types=1); + +namespace Respect\Parameter\Test\Unit; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use Respect\Parameter\Reflector; +use Respect\Parameter\Test\Fixtures\ArrayContainer; +use Respect\Parameter\Test\Fixtures\SampleService; +use Respect\Parameter\Test\Fixtures\ServiceConsumer; + +#[CoversClass(Reflector::class)] +final class ReflectorTest extends TestCase +{ + #[Test] + public function itShouldDetectAcceptedType(): void + { + $constructor = (new ReflectionClass(ServiceConsumer::class))->getConstructor(); + self::assertNotNull($constructor); + + self::assertTrue(Reflector::acceptsType($constructor, SampleService::class)); + self::assertFalse(Reflector::acceptsType($constructor, ArrayContainer::class)); + } + + #[Test] + public function itShouldReflectClosure(): void + { + $fn = static function (string $a): string { + return $a; + }; + + $reflection = Reflector::reflectCallable($fn); + + self::assertInstanceOf(ReflectionFunction::class, $reflection); + self::assertSame('a', $reflection->getParameters()[0]->getName()); + } + + #[Test] + public function itShouldReflectArrayCallable(): void + { + $reflection = Reflector::reflectCallable([new ArrayContainer([]), 'has']); + + self::assertInstanceOf(ReflectionMethod::class, $reflection); + self::assertSame('has', $reflection->getName()); + } + + #[Test] + public function itShouldReflectInvocableObject(): void + { + $invocable = new class () { + public function __invoke(int $x): int + { + return $x; + } + }; + + $reflection = Reflector::reflectCallable($invocable); + + self::assertInstanceOf(ReflectionMethod::class, $reflection); + self::assertSame('__invoke', $reflection->getName()); + self::assertSame('x', $reflection->getParameters()[0]->getName()); + } + + #[Test] + public function itShouldReflectNamedFunction(): void + { + $reflection = Reflector::reflectCallable('strlen'); + + self::assertInstanceOf(ReflectionFunction::class, $reflection); + self::assertSame('strlen', $reflection->getName()); + } + + #[Test] + public function itShouldReflectStaticMethodString(): void + { + $reflection = Reflector::reflectCallable('DateTime::createFromFormat'); + + self::assertInstanceOf(ReflectionMethod::class, $reflection); + self::assertSame('createFromFormat', $reflection->getName()); + } +}