From 9293a26d2f2f0a78c613d0f6e7b549b79549ca45 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Fri, 12 Jun 2026 12:53:37 +0200 Subject: [PATCH] Resolve validator dependencies in the factory instead of validators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A few validators (CountryCode, CurrencyCode, LanguageCode, Phone, SubdivisionCode, and Uuid) fetched their dependencies straight from the ContainerRegistry. That coupled every validator to a global registry and forced anyone providing a custom container to define those services, even when they only wanted to customize something unrelated. Validators now receive their dependencies as optional constructor arguments. The new ContainerArgumentsResolver inspects a constructor and augments the given arguments with whatever the container can provide for the parameters they do not fill, skipping PHP internal classes (such as DateTimeImmutable) that are value-like and must never come from the container. NamespacedValidatorFactory and the Attributes validator share this resolver, so rules created through the fluent API and through PHP attributes both honor services defined in the container. The resolver hides behind the ArgumentsResolver interface, allowing clients to replace the resolution strategy entirely. When no service is available, validators fall back to instantiating their default dependency directly, and they detect missing optional packages with class_exists() instead of container lookups. This makes the MissingComposerDependencyException accurate regardless of how bare the client's container is, but it also means the missing-package paths can no longer be simulated by swapping containers, so the tests doing that were removed. Creating a rule that resolves services from the container costs about 1µs more than before, the price of querying the container and mapping named arguments at creation time. Rules without resolvable parameters pay around 0.1µs, and rules whose arguments already fill the constructor skip the parameter inspection entirely. --- composer.json | 3 +- docs/configuration.md | 54 ++++ docs/validators/Attributes.md | 1 + phpcs.xml.dist | 9 + phpstan.neon.dist | 2 + src-dev/Commands/LintMixinCommand.php | 5 +- .../Linters/ValidatorHeaderLinter.php | 4 + src/ArgumentsResolver.php | 25 ++ src/ContainerArgumentsResolver.php | 128 ++++++++ src/ContainerRegistry.php | 4 + src/NamespacedValidatorFactory.php | 19 +- src/Validators/Attributes.php | 28 +- src/Validators/CountryCode.php | 9 +- src/Validators/CurrencyCode.php | 9 +- src/Validators/LanguageCode.php | 9 +- src/Validators/Phone.php | 29 +- src/Validators/SubdivisionCode.php | 13 +- src/Validators/Uuid.php | 13 +- .../Functions/functionWithNonExistentType.php | 18 ++ .../namedFunctionWithTransformer.php | 18 ++ tests/src/Validators/Injectable.php | 30 ++ tests/src/Validators/InjectableWithPdo.php | 30 ++ tests/unit/ContainerArgumentsResolverTest.php | 291 ++++++++++++++++++ tests/unit/NamespacedRuleFactoryTest.php | 54 ++++ tests/unit/Validators/AttributesTest.php | 42 +++ tests/unit/Validators/CountryCodeTest.php | 21 -- tests/unit/Validators/CurrencyCodeTest.php | 21 -- tests/unit/Validators/LanguageCodeTest.php | 21 -- tests/unit/Validators/PhoneTest.php | 55 ---- tests/unit/Validators/SubdivisionCodeTest.php | 21 -- tests/unit/Validators/UuidTest.php | 21 -- 31 files changed, 801 insertions(+), 206 deletions(-) create mode 100644 src/ArgumentsResolver.php create mode 100644 src/ContainerArgumentsResolver.php create mode 100644 tests/src/Functions/functionWithNonExistentType.php create mode 100644 tests/src/Functions/namedFunctionWithTransformer.php create mode 100644 tests/src/Validators/Injectable.php create mode 100644 tests/src/Validators/InjectableWithPdo.php create mode 100644 tests/unit/ContainerArgumentsResolverTest.php diff --git a/composer.json b/composer.json index 68ac3fdff..bda86faea 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,8 @@ "Respect\\Dev\\": "src-dev/", "Respect\\Validation\\": "tests/unit/", "Respect\\Validation\\Test\\": "tests/src/" - } + }, + "files": ["tests/src/Functions/namedFunctionWithTransformer.php"] }, "scripts": { "bench-profile": "vendor/bin/phpbench xdebug:profile", diff --git a/docs/configuration.md b/docs/configuration.md index aaa3d818c..b08457125 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,6 +2,7 @@ SPDX-License-Identifier: MIT SPDX-FileCopyrightText: (c) Respect Project Contributors SPDX-FileContributor: Alexandre Gomes Gaigalas +SPDX-FileContributor: Henrique Moody --> # Configuration @@ -17,3 +18,56 @@ use Respect\Validation\ContainerRegistry; ContainerRegistry::setContainer($yourPsr11Container); ``` + +## Service injection + +When a validator is created — through the fluent API or through PHP attributes — the constructor parameters that are not filled by the given arguments are resolved from the container, as long as the parameter type is a class or interface the container can provide. + +Some bundled validators rely on services that you might want to customize: + +| Validator | Service | +| :----------------------------------------------- | :-------------------------------------------------------------------------- | +| [CountryCode](validators/CountryCode.md) | `Sokil\IsoCodes\Database\Countries` | +| [CurrencyCode](validators/CurrencyCode.md) | `Sokil\IsoCodes\Database\Currencies` | +| [Email](validators/Email.md) | `Egulias\EmailValidator\EmailValidator` | +| [LanguageCode](validators/LanguageCode.md) | `Sokil\IsoCodes\Database\Languages` | +| [Phone](validators/Phone.md) | `libphonenumber\PhoneNumberUtil`, `Sokil\IsoCodes\Database\Countries` | +| [SubdivisionCode](validators/SubdivisionCode.md) | `Sokil\IsoCodes\Database\Countries`, `Sokil\IsoCodes\Database\Subdivisions` | +| [Uuid](validators/Uuid.md) | `Ramsey\Uuid\UuidFactory` | + +You do not need to define any of those services yourself: when the container does not provide a service, validators fall back to a sensible default. Define a service only when you want to customize it: + +```php +use Respect\Validation\ContainerRegistry; +use Sokil\IsoCodes\Database\Countries; +use Sokil\IsoCodes\TranslationDriver\SymfonyTranslationDriver; + +ContainerRegistry::setContainer(ContainerRegistry::createContainer([ + Countries::class => new Countries(null, new SymfonyTranslationDriver()), +])); + +v::countryCode()->assert('BR'); +``` + +Note that a few kinds of constructor parameters are never injected: + +- Parameters already filled by the given arguments. +- Variadic and union-typed parameters. +- Parameters whose type is in the unresolvable types list, which are value-like or rule-like and must come from the arguments or from the parameter default. By default, the list includes `DateTimeImmutable`, `DateTime`, `DateTimeInterface`, and `Validator`. + +You can customize the unresolvable types by overriding the `ArgumentsResolver` service in the container — see [Custom arguments resolver](#custom-arguments-resolver) below. + +## Custom arguments resolver + +The injection behavior is defined by the `Respect\Validation\ArgumentsResolver` interface, whose default implementation is `Respect\Validation\ContainerArgumentsResolver`. If you want a different resolution strategy — creating services directly instead of going through a container, for example — define your own implementation in the container: + +```php +use Respect\Validation\ArgumentsResolver; +use Respect\Validation\ContainerRegistry; + +ContainerRegistry::setContainer(ContainerRegistry::createContainer([ + ArgumentsResolver::class => new MyArgumentsResolver(), +])); +``` + +Both the validator factory and the [Attributes](validators/Attributes.md) validator use the resolver, so a custom implementation affects rules created through the fluent API and through PHP attributes alike. diff --git a/docs/validators/Attributes.md b/docs/validators/Attributes.md index 012ef88dd..5b3186a3b 100644 --- a/docs/validators/Attributes.md +++ b/docs/validators/Attributes.md @@ -75,6 +75,7 @@ v::attributes()->assert(new Person('', 'not a date', 'not an email', 'not a phon - If the object has no attributes, the validation will always pass. - When the property is nullable, this validator will wrap the validator on the property into [NullOr](NullOr.md) validator. - This validator has no templates because it uses the templates of the validators that are applied to the properties. +- When created with `v::attributes()`, the validators used as attributes will have their service dependencies — such as `PhoneNumberUtil` for [Phone](Phone.md) — resolved according to the [container configuration](../configuration.md). When instantiated directly with `new Attributes()`, attribute validators are created with their default dependencies instead. ## Categorization diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 20d17139b..0f795f271 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -36,4 +36,13 @@ tests/Pest.php tests/feature + + tests/src/Functions/functionWithNonExistentType.php + + + tests/src/Functions/namedFunctionWithTransformer.php + + + tests/src/Functions/namedFunctionWithTransformer.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 47b7facab..0efeb7f63 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,6 +9,8 @@ parameters: # reading them in tests, so treat TestCall as a property bag. - Pest\PendingCalls\TestCall ignoreErrors: + - identifier: class.notFound + path: tests/src/Functions/functionWithNonExistentType.php - # Why: SimpleXMLElement is weird and doesn't implement anything ArrayAccess-like message: '/Instanceof between mixed and SimpleXMLElement will always evaluate to false\./' diff --git a/src-dev/Commands/LintMixinCommand.php b/src-dev/Commands/LintMixinCommand.php index d9c6828de..4c299f038 100644 --- a/src-dev/Commands/LintMixinCommand.php +++ b/src-dev/Commands/LintMixinCommand.php @@ -18,6 +18,7 @@ use Respect\Dev\CodeGen\InterfaceConfig; use Respect\Dev\Differ\ConsoleDiffer; use Respect\Dev\Differ\Item; +use Respect\Validation\ArgumentsResolver; use Respect\Validation\Mixins\Chain; use Respect\Validation\Validator; use Respect\Validation\ValidatorBuilder; @@ -70,8 +71,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $generator = new MixinGenerator( config: $config, methodBuilder: new MethodBuilder( - excludedTypePrefixes: ['Sokil', 'Egulias'], - excludedTypeNames: ['finfo'], + excludedTypePrefixes: ['Sokil', 'Egulias', 'libphonenumber', 'Ramsey'], + excludedTypeNames: ['finfo', ArgumentsResolver::class], ), interfaces: [ new InterfaceConfig( diff --git a/src-dev/Markdown/Linters/ValidatorHeaderLinter.php b/src-dev/Markdown/Linters/ValidatorHeaderLinter.php index ce6b61871..e82e8008e 100644 --- a/src-dev/Markdown/Linters/ValidatorHeaderLinter.php +++ b/src-dev/Markdown/Linters/ValidatorHeaderLinter.php @@ -18,6 +18,7 @@ use ReflectionUnionType; use Respect\Dev\Markdown\File; use Respect\Dev\Markdown\Linter; +use Respect\Validation\ArgumentsResolver; use function array_filter; use function array_keys; @@ -174,6 +175,9 @@ private function getParameter(ReflectionParameter $reflection): array|null if ( str_starts_with($type->getName(), 'Sokil') || str_starts_with($type->getName(), 'Egulias') + || str_starts_with($type->getName(), 'libphonenumber') + || str_starts_with($type->getName(), 'Ramsey') + || $type->getName() === ArgumentsResolver::class || $type->getName() === 'finfo' ) { return null; diff --git a/src/ArgumentsResolver.php b/src/ArgumentsResolver.php new file mode 100644 index 000000000..8cd3d5ca5 --- /dev/null +++ b/src/ArgumentsResolver.php @@ -0,0 +1,25 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use ReflectionFunctionAbstract; + +interface ArgumentsResolver +{ + /** + * Augments the given arguments with values for the parameters they do not already fill. + * + * @param array $arguments + * + * @return array + */ + public function resolve(ReflectionFunctionAbstract $function, array $arguments): array; +} diff --git a/src/ContainerArgumentsResolver.php b/src/ContainerArgumentsResolver.php new file mode 100644 index 000000000..825f6cd2c --- /dev/null +++ b/src/ContainerArgumentsResolver.php @@ -0,0 +1,128 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +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; + +final class ContainerArgumentsResolver implements ArgumentsResolver +{ + /** @var array> */ + private array $injectableParametersCache = []; + + /** @param array> $unresolvableTypes */ + public function __construct( + private readonly ContainerInterface $container, + private readonly array $unresolvableTypes = [ + DateTimeImmutable::class, + DateTime::class, + DateTimeInterface::class, + Validator::class, + ], + ) { + } + + /** + * @param array $arguments + * + * @return array + */ + public function resolve(ReflectionFunctionAbstract $function, array $arguments): array + { + if (count($arguments) >= $function->getNumberOfParameters()) { + return $arguments; + } + + $injectableParameters = $this->injectableParameters($function); + if ($injectableParameters === []) { + return $arguments; + } + + $positionalArgumentsCount = count( + array_is_list($arguments) ? $arguments : array_filter(array_keys($arguments), is_int(...)), + ); + + foreach ($injectableParameters 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 injectableParameters(ReflectionFunctionAbstract $function): array + { + $cacheKey = $this->createCacheKey($function); + if (isset($this->injectableParametersCache[$cacheKey])) { + return $this->injectableParametersCache[$cacheKey]; + } + + $parameters = []; + foreach ($function->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->injectableParametersCache[$cacheKey] = $parameters; + } + + private function createCacheKey(ReflectionFunctionAbstract $function): string + { + if ($function instanceof ReflectionMethod) { + return $function->class . '::' . $function->name; + } + + if (!$function->isClosure()) { + return $function->name; + } + + $file = $function->getFileName() ?: 'internal'; + $line = $function->getStartLine() ?: 0; + + return $function->getName() . '@' . $file . ':' . $line; + } +} diff --git a/src/ContainerRegistry.php b/src/ContainerRegistry.php index 0dcd8c536..29c61f2cf 100644 --- a/src/ContainerRegistry.php +++ b/src/ContainerRegistry.php @@ -68,9 +68,13 @@ public static function createContainer(array $definitions = []): Container 'respect.validation.formatter.messages' => autowire(NestedArrayFormatter::class), 'respect.validation.ignored_backtrace_paths' => [__DIR__ . '/ValidatorBuilder.php'], 'respect.validation.rule_factory.namespaces' => ['Respect\\Validation\\Validators'], + ArgumentsResolver::class => factory( + static fn(Container $container) => new ContainerArgumentsResolver($container), + ), ValidatorFactory::class => factory(static fn(Container $container) => new NamespacedValidatorFactory( $container->get(Transformer::class), $container->get('respect.validation.rule_factory.namespaces'), + $container->get(ArgumentsResolver::class), )), Quoter::class => create(CodeQuoter::class)->constructor(120), Handler::class => factory(static function (Container $container) { diff --git a/src/NamespacedValidatorFactory.php b/src/NamespacedValidatorFactory.php index 369dd8994..5be386832 100644 --- a/src/NamespacedValidatorFactory.php +++ b/src/NamespacedValidatorFactory.php @@ -30,6 +30,7 @@ public function __construct( private Transformer $transformer, private array $rulesNamespaces, + private ArgumentsResolver|null $argumentsResolver = null, ) { } @@ -88,11 +89,27 @@ private function createRule(string $ruleName, array $arguments = []): Validator } try { - return $reflection->newInstanceArgs($arguments); + return $reflection->newInstanceArgs($this->resolveArguments($reflection, $arguments)); } catch (ReflectionException) { throw new InvalidClassException( sprintf('"%s" could not be instantiated with arguments %s', $ruleName, stringify($arguments)), ); } } + + /** + * @param ReflectionClass $reflection + * @param array $arguments + * + * @return array + */ + private function resolveArguments(ReflectionClass $reflection, array $arguments): array + { + $constructor = $reflection->getConstructor(); + if ($constructor === null || $this->argumentsResolver === null) { + return $arguments; + } + + return $this->argumentsResolver->resolve($constructor, $arguments); + } } diff --git a/src/Validators/Attributes.php b/src/Validators/Attributes.php index 78ff90583..35e4130eb 100644 --- a/src/Validators/Attributes.php +++ b/src/Validators/Attributes.php @@ -18,6 +18,7 @@ use ReflectionObject; use ReflectionProperty; use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Validation\ArgumentsResolver; use Respect\Validation\Id; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -27,6 +28,11 @@ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class Attributes implements Validator { + public function __construct( + private readonly ArgumentsResolver|null $argumentsResolver = null, + ) { + } + public function evaluate(mixed $input): Result { $id = new Id('attributes'); @@ -50,7 +56,7 @@ private function getClassValidators(ReflectionObject $reflection): array $validators = []; while ($reflection instanceof ReflectionClass) { foreach ($reflection->getAttributes(Validator::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $validators[] = $attribute->newInstance(); + $validators[] = $this->createValidator($attribute); } $reflection = $reflection->getParentClass(); @@ -66,7 +72,7 @@ private function getPropertyValidators(ReflectionObject $reflection): array foreach ($this->getProperties($reflection) as $propertyName => $property) { $propertyValidators = []; foreach ($property->getAttributes(Validator::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $propertyValidators[] = $attribute->newInstance(); + $propertyValidators[] = $this->createValidator($attribute); } if ($propertyValidators === []) { @@ -82,6 +88,24 @@ private function getPropertyValidators(ReflectionObject $reflection): array return $validators; } + /** @param ReflectionAttribute $attribute */ + private function createValidator(ReflectionAttribute $attribute): Validator + { + if ($this->argumentsResolver === null) { + return $attribute->newInstance(); + } + + $reflection = new ReflectionClass($attribute->getName()); + $constructor = $reflection->getConstructor(); + if ($constructor === null) { + return $attribute->newInstance(); + } + + return $reflection->newInstanceArgs( + $this->argumentsResolver->resolve($constructor, $attribute->getArguments()), + ); + } + /** @return array */ private function getProperties(ReflectionObject $reflection): array { diff --git a/src/Validators/CountryCode.php b/src/Validators/CountryCode.php index 93894f565..4e8b042f3 100644 --- a/src/Validators/CountryCode.php +++ b/src/Validators/CountryCode.php @@ -18,8 +18,6 @@ namespace Respect\Validation\Validators; use Attribute; -use Psr\Container\NotFoundExceptionInterface; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Message\Template; @@ -27,6 +25,7 @@ use Respect\Validation\Validator; use Sokil\IsoCodes\Database\Countries; +use function class_exists; use function in_array; use function is_string; @@ -53,15 +52,15 @@ public function __construct( ); } - try { - $this->countries = $countries ?? ContainerRegistry::getContainer()->get(Countries::class); - } catch (NotFoundExceptionInterface) { + if ($countries === null && !class_exists(Countries::class)) { throw new MissingComposerDependencyException( 'CountryCode rule requires PHP ISO Codes', 'sokil/php-isocodes', 'sokil/php-isocodes-db-only', ); } + + $this->countries = $countries ?? new Countries(); } public function evaluate(mixed $input): Result diff --git a/src/Validators/CurrencyCode.php b/src/Validators/CurrencyCode.php index 74af78c08..99381462b 100644 --- a/src/Validators/CurrencyCode.php +++ b/src/Validators/CurrencyCode.php @@ -15,8 +15,6 @@ namespace Respect\Validation\Validators; use Attribute; -use Psr\Container\NotFoundExceptionInterface; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Message\Template; @@ -24,6 +22,7 @@ use Respect\Validation\Validator; use Sokil\IsoCodes\Database\Currencies; +use function class_exists; use function in_array; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] @@ -49,15 +48,15 @@ public function __construct( ); } - try { - $this->currencies = $currencies ?? ContainerRegistry::getContainer()->get(Currencies::class); - } catch (NotFoundExceptionInterface) { + if ($currencies === null && !class_exists(Currencies::class)) { throw new MissingComposerDependencyException( 'CurrencyCode rule requires PHP ISO Codes', 'sokil/php-isocodes', 'sokil/php-isocodes-db-only', ); } + + $this->currencies = $currencies ?? new Currencies(); } public function evaluate(mixed $input): Result diff --git a/src/Validators/LanguageCode.php b/src/Validators/LanguageCode.php index 0199c1ae9..18bf3574a 100644 --- a/src/Validators/LanguageCode.php +++ b/src/Validators/LanguageCode.php @@ -14,8 +14,6 @@ namespace Respect\Validation\Validators; use Attribute; -use Psr\Container\NotFoundExceptionInterface; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Message\Template; @@ -23,6 +21,7 @@ use Respect\Validation\Validator; use Sokil\IsoCodes\Database\Languages; +use function class_exists; use function in_array; use function is_string; @@ -49,15 +48,15 @@ public function __construct( ); } - try { - $this->languages = $languages ?? ContainerRegistry::getContainer()->get(Languages::class); - } catch (NotFoundExceptionInterface) { + if ($languages === null && !class_exists(Languages::class)) { throw new MissingComposerDependencyException( 'LanguageCode rule requires PHP ISO Codes', 'sokil/php-isocodes', 'sokil/php-isocodes-db-only', ); } + + $this->languages = $languages ?? new Languages(); } public function evaluate(mixed $input): Result diff --git a/src/Validators/Phone.php b/src/Validators/Phone.php index 78f8f3881..a21fb64c3 100644 --- a/src/Validators/Phone.php +++ b/src/Validators/Phone.php @@ -21,8 +21,6 @@ use Attribute; use libphonenumber\NumberParseException; use libphonenumber\PhoneNumberUtil; -use Psr\Container\NotFoundExceptionInterface; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Message\Template; @@ -30,6 +28,7 @@ use Respect\Validation\Validator; use Sokil\IsoCodes\Database\Countries; +use function class_exists; use function is_scalar; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] @@ -50,24 +49,29 @@ final class Phone implements Validator private readonly Countries\Country|null $country; - public function __construct(string|null $countryCode = null, Countries|null $countries = null) - { - if (!ContainerRegistry::getContainer()->has(PhoneNumberUtil::class)) { + private readonly PhoneNumberUtil $phoneNumberUtil; + + public function __construct( + string|null $countryCode = null, + Countries|null $countries = null, + PhoneNumberUtil|null $phoneNumberUtil = null, + ) { + if ($phoneNumberUtil === null && !class_exists(PhoneNumberUtil::class)) { throw new MissingComposerDependencyException( 'Phone rule requires libphonenumber for PHP', 'giggsey/libphonenumber-for-php', ); } + $this->phoneNumberUtil = $phoneNumberUtil ?? PhoneNumberUtil::getInstance(); + if ($countryCode == null) { $this->country = null; return; } - try { - $countries ??= ContainerRegistry::getContainer()->get(Countries::class); - } catch (NotFoundExceptionInterface) { + if ($countries === null && !class_exists(Countries::class)) { throw new MissingComposerDependencyException( 'Phone rule with country code requires PHP ISO Codes', 'sokil/php-isocodes', @@ -75,7 +79,7 @@ public function __construct(string|null $countryCode = null, Countries|null $cou ); } - $this->country = $countries->getByAlpha2($countryCode); + $this->country = ($countries ?? new Countries())->getByAlpha2($countryCode); if ($this->country === null) { throw new InvalidValidatorException('Invalid country code %s', $countryCode); } @@ -95,13 +99,12 @@ public function evaluate(mixed $input): Result private function isValidPhone(string $input): bool { try { - $phoneNumberUtil = ContainerRegistry::getContainer()->get(PhoneNumberUtil::class); - $phoneNumberObject = $phoneNumberUtil->parse($input, $this->country?->getAlpha2()); + $phoneNumberObject = $this->phoneNumberUtil->parse($input, $this->country?->getAlpha2()); if ($this->country === null) { - return $phoneNumberUtil->isValidNumber($phoneNumberObject); + return $this->phoneNumberUtil->isValidNumber($phoneNumberObject); } - return $phoneNumberUtil->getRegionCodeForNumber($phoneNumberObject) === $this->country->getAlpha2(); + return $this->phoneNumberUtil->getRegionCodeForNumber($phoneNumberObject) === $this->country->getAlpha2(); } catch (NumberParseException) { } diff --git a/src/Validators/SubdivisionCode.php b/src/Validators/SubdivisionCode.php index da3c721f1..fec5a4b2d 100644 --- a/src/Validators/SubdivisionCode.php +++ b/src/Validators/SubdivisionCode.php @@ -12,8 +12,6 @@ namespace Respect\Validation\Validators; use Attribute; -use Psr\Container\NotFoundExceptionInterface; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Helpers\CanValidateUndefined; @@ -23,6 +21,8 @@ use Sokil\IsoCodes\Database\Countries; use Sokil\IsoCodes\Database\Subdivisions; +use function class_exists; + #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a subdivision code of {{countryName|trans}}', @@ -41,11 +41,7 @@ public function __construct( Countries|null $countries = null, Subdivisions|null $subdivisions = null, ) { - try { - $container = ContainerRegistry::getContainer(); - $countries ??= $container->get(Countries::class); - $this->subdivisions = $subdivisions ?? $container->get(Subdivisions::class); - } catch (NotFoundExceptionInterface) { + if (($countries === null || $subdivisions === null) && !class_exists(Subdivisions::class)) { throw new MissingComposerDependencyException( 'SubdivisionCode rule requires PHP ISO Codes', 'sokil/php-isocodes', @@ -53,6 +49,9 @@ public function __construct( ); } + $countries ??= new Countries(); + $this->subdivisions = $subdivisions ?? new Subdivisions(); + $country = $countries->getByAlpha2($countryCode); if ($country === null) { throw new InvalidValidatorException('"%s" is not a supported country code', $countryCode); diff --git a/src/Validators/Uuid.php b/src/Validators/Uuid.php index 654df0a0c..6a5296afd 100644 --- a/src/Validators/Uuid.php +++ b/src/Validators/Uuid.php @@ -22,7 +22,6 @@ use Ramsey\Uuid\Rfc4122\FieldsInterface; use Ramsey\Uuid\UuidFactory; use Ramsey\Uuid\UuidInterface; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Message\Template; @@ -30,6 +29,7 @@ use Respect\Validation\Validator; use Throwable; +use function class_exists; use function is_string; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] @@ -47,16 +47,21 @@ final class Uuid implements Validator { public const string TEMPLATE_VERSION = '__version__'; + private readonly UuidFactory $uuidFactory; + public function __construct( private readonly int|null $version = null, + UuidFactory|null $uuidFactory = null, ) { - if (!ContainerRegistry::getContainer()->has(UuidFactory::class)) { + if ($uuidFactory === null && !class_exists(UuidFactory::class)) { throw new MissingComposerDependencyException( 'Uuid rule requires ramsey/uuid package', 'ramsey/uuid', ); } + $this->uuidFactory = $uuidFactory ?? new UuidFactory(); + if ($version !== null && !$this->isSupportedVersion($version)) { throw new InvalidValidatorException( 'Only versions 1 to 8 are supported: %d given', @@ -75,9 +80,7 @@ public function evaluate(mixed $input): Result } try { - $uuid = is_string($input) ? ContainerRegistry::getContainer() - ->get(UuidFactory::class) - ->fromString($input) : $input; + $uuid = is_string($input) ? $this->uuidFactory->fromString($input) : $input; } catch (Throwable) { return Result::failed($input, $this, $parameters, $template); } diff --git a/tests/src/Functions/functionWithNonExistentType.php b/tests/src/Functions/functionWithNonExistentType.php new file mode 100644 index 000000000..5aa0b3768 --- /dev/null +++ b/tests/src/Functions/functionWithNonExistentType.php @@ -0,0 +1,18 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Test\Functions; + +use NonExistentClass123; + +function functionWithNonExistentType(NonExistentClass123 $x): bool +{ + return true; +} diff --git a/tests/src/Functions/namedFunctionWithTransformer.php b/tests/src/Functions/namedFunctionWithTransformer.php new file mode 100644 index 000000000..a1549e66f --- /dev/null +++ b/tests/src/Functions/namedFunctionWithTransformer.php @@ -0,0 +1,18 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Test\Functions; + +use Respect\Validation\Transformers\Transformer; + +function namedFunctionWithTransformer(string $name, Transformer $transformer): bool +{ + return true; +} diff --git a/tests/src/Validators/Injectable.php b/tests/src/Validators/Injectable.php new file mode 100644 index 000000000..29eb18658 --- /dev/null +++ b/tests/src/Validators/Injectable.php @@ -0,0 +1,30 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Test\Validators; + +use Attribute; +use Respect\Validation\Transformers\Transformer; +use Respect\Validation\Validators\Core\Simple; + +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class Injectable extends Simple +{ + public function __construct( + public readonly string $name = 'default', + public readonly Transformer|null $transformer = null, + ) { + } + + public function isValid(mixed $input): bool + { + return $this->transformer !== null; + } +} diff --git a/tests/src/Validators/InjectableWithPdo.php b/tests/src/Validators/InjectableWithPdo.php new file mode 100644 index 000000000..a7490c741 --- /dev/null +++ b/tests/src/Validators/InjectableWithPdo.php @@ -0,0 +1,30 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Test\Validators; + +use Attribute; +use PDO; +use Respect\Validation\Validators\Core\Simple; + +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class InjectableWithPdo extends Simple +{ + public function __construct( + public readonly string $name = 'default', + public readonly PDO|null $pdo = null, + ) { + } + + public function isValid(mixed $input): bool + { + return $this->pdo !== null; + } +} diff --git a/tests/unit/ContainerArgumentsResolverTest.php b/tests/unit/ContainerArgumentsResolverTest.php new file mode 100644 index 000000000..f77a47b4b --- /dev/null +++ b/tests/unit/ContainerArgumentsResolverTest.php @@ -0,0 +1,291 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use DateTimeImmutable; +use DI\Container; +use PDO; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use ReflectionFunction; +use ReflectionMethod; +use Respect\Validation\Test\TestCase; +use Respect\Validation\Test\Transformers\StubTransformer; +use Respect\Validation\Test\Validators\Injectable; +use Respect\Validation\Test\Validators\InjectableWithPdo; +use Respect\Validation\Transformers\Transformer; +use Respect\Validation\Validators\AlwaysValid; +use Respect\Validation\Validators\DateTimeDiff; +use Respect\Validation\Validators\Not; + +use function Respect\Validation\Test\Functions\namedFunctionWithTransformer; + +#[Group('core')] +#[CoversClass(ContainerArgumentsResolver::class)] +final class ContainerArgumentsResolverTest extends TestCase +{ + #[Test] + public function shouldAugmentArgumentsWithContainerValuesForUnfilledParameters(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + self::assertSame( + ['some name', 'transformer' => $transformer], + $resolver->resolve(self::constructor(Injectable::class), ['some name']), + ); + } + + #[Test] + public function shouldReturnArgumentsUnchangedWhenPositionalArgumentsFillAllParameters(): void + { + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => new StubTransformer()])); + $arguments = ['some name', new StubTransformer()]; + + self::assertSame($arguments, $resolver->resolve(self::constructor(Injectable::class), $arguments)); + } + + #[Test] + public function shouldReturnArgumentsUnchangedWhenNamedArgumentsFillInjectableParameters(): void + { + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => new StubTransformer()])); + $arguments = ['transformer' => new StubTransformer()]; + + self::assertSame($arguments, $resolver->resolve(self::constructor(Injectable::class), $arguments)); + } + + #[Test] + public function shouldReturnArgumentsUnchangedWhenContainerDoesNotHaveParameterType(): void + { + $resolver = new ContainerArgumentsResolver(new Container()); + + self::assertSame([], $resolver->resolve(self::constructor(Injectable::class), [])); + } + + #[Test] + public function shouldNotResolveUnresolvableTypesFromContainer(): void + { + $container = new Container(); + $resolver = new ContainerArgumentsResolver($container); + $arguments = ['years', new AlwaysValid()]; + + self::assertSame($arguments, $resolver->resolve(self::constructor(DateTimeDiff::class), $arguments)); + self::assertTrue($container->has(DateTimeImmutable::class)); + } + + #[Test] + public function shouldNotResolveValidatorFromContainer(): void + { + $validator = new AlwaysValid(); + $container = new Container([Validator::class => $validator]); + $resolver = new ContainerArgumentsResolver($container); + + $result = $resolver->resolve(self::constructor(Not::class), []); + + self::assertSame([], $result); + self::assertTrue($container->has(Validator::class)); + } + + #[Test] + public function shouldResolveInternalClassTypesFromContainerWhenNotUnresolvable(): void + { + $pdo = new PDO('sqlite::memory:'); + $container = new Container([PDO::class => $pdo]); + $resolver = new ContainerArgumentsResolver($container); + + $result = $resolver->resolve(self::constructor(InjectableWithPdo::class), ['some name']); + + self::assertSame(['some name', 'pdo' => $pdo], $result); + } + + #[Test] + public function shouldUseCustomUnresolvableTypes(): void + { + $pdo = new PDO('sqlite::memory:'); + $container = new Container([PDO::class => $pdo]); + $resolver = new ContainerArgumentsResolver($container, [PDO::class]); + + self::assertSame( + ['some name'], + $resolver->resolve(self::constructor(InjectableWithPdo::class), ['some name']), + ); + } + + #[Test] + public function shouldAllowOverridingDefaultUnresolvableTypes(): void + { + $transformer = new StubTransformer(); + $container = new Container([Transformer::class => $transformer]); + $resolver = new ContainerArgumentsResolver($container, []); + + $result = $resolver->resolve(self::constructor(Injectable::class), ['some name']); + + self::assertSame(['some name', 'transformer' => $transformer], $result); + } + + #[Test] + public function shouldResolveInjectableParameterFromContainerForClosure(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + $closure = static fn(string $name, Transformer $transformer): bool => true; + + self::assertSame( + ['some name', 'transformer' => $transformer], + $resolver->resolve(new ReflectionFunction($closure), ['some name']), + ); + } + + #[Test] + public function shouldReturnArgumentsUnchangedForClosureWhenAllParametersAreFilled(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + $closure = static fn(string $name, Transformer $transformer): bool => true; + $arguments = ['some name', $transformer]; + + self::assertSame($arguments, $resolver->resolve(new ReflectionFunction($closure), $arguments)); + } + + #[Test] + public function shouldReturnArgumentsUnchangedForClosureWhenContainerDoesNotHaveParameterType(): void + { + $resolver = new ContainerArgumentsResolver(new Container()); + + $closure = static fn(string $name, Transformer $transformer): bool => true; + + self::assertSame([], $resolver->resolve(new ReflectionFunction($closure), [])); + } + + #[Test] + public function shouldNotResolveBuiltInTypesForClosure(): void + { + $resolver = new ContainerArgumentsResolver(new Container()); + + $closure = static fn(string $name, int $count): bool => true; + + self::assertSame([], $resolver->resolve(new ReflectionFunction($closure), [])); + } + + #[Test] + public function shouldNotResolveUnresolvableTypesForClosure(): void + { + $container = new Container(); + $resolver = new ContainerArgumentsResolver($container); + + $closure = static fn(DateTimeImmutable $date): bool => true; + + self::assertSame([], $resolver->resolve(new ReflectionFunction($closure), [])); + self::assertTrue($container->has(DateTimeImmutable::class)); + } + + #[Test] + public function shouldResolveNamedArgumentsForClosure(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + $closure = static fn(string $name, Transformer $transformer): bool => true; + + self::assertSame( + ['transformer' => $transformer], + $resolver->resolve(new ReflectionFunction($closure), ['transformer' => $transformer]), + ); + } + + #[Test] + public function shouldResolveInjectableParameterFromContainerForNamedFunction(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + $function = new ReflectionFunction(namedFunctionWithTransformer(...)); + + self::assertSame( + ['some name', 'transformer' => $transformer], + $resolver->resolve($function, ['some name']), + ); + } + + #[Test] + public function shouldReturnArgumentsUnchangedForNamedFunctionWhenAllParametersAreFilled(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + $function = new ReflectionFunction(namedFunctionWithTransformer(...)); + $arguments = ['some name', $transformer]; + + self::assertSame($arguments, $resolver->resolve($function, $arguments)); + } + + #[Test] + public function shouldReturnArgumentsUnchangedForNamedFunctionWhenContainerDoesNotHaveParameterType(): void + { + $resolver = new ContainerArgumentsResolver(new Container()); + + $function = new ReflectionFunction(namedFunctionWithTransformer(...)); + + self::assertSame([], $resolver->resolve($function, [])); + } + + #[Test] + public function shouldUseCachedInjectableParametersOnSubsequentCalls(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + $constructor = self::constructor(Injectable::class); + + // First call populates the cache + $resolver->resolve($constructor, ['some name']); + + // Second call should hit the cache (line 88) + $result = $resolver->resolve($constructor, ['another name']); + + self::assertSame(['another name', 'transformer' => $transformer], $result); + } + + #[Test] + public function shouldSkipParametersWithNonClassOrInterfaceType(): void + { + $resolver = new ContainerArgumentsResolver(new Container()); + + require_once __DIR__ . '/../src/Functions/functionWithNonExistentType.php'; + + $function = new ReflectionFunction('\Respect\Validation\Test\Functions\functionWithNonExistentType'); + + self::assertSame([], $resolver->resolve($function, [])); + } + + #[Test] + public function shouldCreateCacheKeyForNamedFunction(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + // Use string name to create ReflectionFunction for a named function (not a Closure) + $function = new ReflectionFunction('Respect\Validation\Test\Functions\namedFunctionWithTransformer'); + + $result = $resolver->resolve($function, ['some name']); + + self::assertSame(['some name', 'transformer' => $transformer], $result); + } + + /** @param class-string $className */ + private static function constructor(string $className): ReflectionMethod + { + return new ReflectionMethod($className, '__construct'); + } +} diff --git a/tests/unit/NamespacedRuleFactoryTest.php b/tests/unit/NamespacedRuleFactoryTest.php index 3dc428190..a63a4c63e 100644 --- a/tests/unit/NamespacedRuleFactoryTest.php +++ b/tests/unit/NamespacedRuleFactoryTest.php @@ -12,6 +12,7 @@ namespace Respect\Validation; +use DI\Container; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; @@ -19,10 +20,12 @@ use Respect\Validation\Exceptions\InvalidClassException; use Respect\Validation\Test\TestCase; use Respect\Validation\Test\Transformers\StubTransformer; +use Respect\Validation\Test\Validators\Injectable; use Respect\Validation\Test\Validators\Invalid; use Respect\Validation\Test\Validators\MyAbstractClass; use Respect\Validation\Test\Validators\Stub; use Respect\Validation\Test\Validators\Valid; +use Respect\Validation\Transformers\Transformer; use function assert; use function sprintf; @@ -62,6 +65,57 @@ public function shouldDefineConstructorArgumentsWhenCreatingRule(): void self::assertSame($constructorArguments, $validator->validations); } + #[Test] + public function shouldInjectServicesFromContainerIntoUnfilledConstructorParameters(): void + { + $transformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => $transformer])); + + $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE], $resolver); + $validator = $factory->create('injectable', ['some name']); + assert($validator instanceof Injectable); + + self::assertSame('some name', $validator->name); + self::assertSame($transformer, $validator->transformer); + } + + #[Test] + public function shouldNotInjectServicesIntoParametersFilledByArguments(): void + { + $argumentTransformer = new StubTransformer(); + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => new StubTransformer()])); + + $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE], $resolver); + $validator = $factory->create('injectable', ['some name', $argumentTransformer]); + assert($validator instanceof Injectable); + + self::assertSame($argumentTransformer, $validator->transformer); + } + + #[Test] + public function shouldNotInjectServicesWhenCreatedWithoutArgumentsResolver(): void + { + $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); + $validator = $factory->create('injectable'); + assert($validator instanceof Injectable); + + self::assertNull($validator->transformer); + } + + #[Test] + public function shouldNotInjectServicesTheContainerDoesNotHave(): void + { + $factory = new NamespacedValidatorFactory( + new StubTransformer(), + [self::TEST_RULES_NAMESPACE], + new ContainerArgumentsResolver(new Container()), + ); + $validator = $factory->create('injectable'); + assert($validator instanceof Injectable); + + self::assertNull($validator->transformer); + } + #[Test] public function shouldThrowsAnExceptionOnConstructorReflectionFailure(): void { diff --git a/tests/unit/Validators/AttributesTest.php b/tests/unit/Validators/AttributesTest.php index cd4a34970..0ff63209c 100644 --- a/tests/unit/Validators/AttributesTest.php +++ b/tests/unit/Validators/AttributesTest.php @@ -11,12 +11,17 @@ namespace Respect\Validation\Validators; +use DI\Container; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; +use Respect\Validation\ContainerArgumentsResolver; use Respect\Validation\Test\Stubs\WithAttributes; use Respect\Validation\Test\TestCase; +use Respect\Validation\Test\Transformers\StubTransformer; +use Respect\Validation\Test\Validators\Injectable; +use Respect\Validation\Transformers\Transformer; #[Group(' rule')] #[CoversClass(Attributes::class)] @@ -50,6 +55,43 @@ public function shouldNotEvaluateObjectsWithInvalidPropertyValues(object $input) self::assertInvalidInput(new Attributes(), $input); } + #[Test] + public function shouldCreateAttributeValidatorsWithArgumentsFromContainerWhenResolverIsGiven(): void + { + $resolver = new ContainerArgumentsResolver(new Container([Transformer::class => new StubTransformer()])); + + $input = new class { + #[Injectable('some name')] + public string $property = 'value'; + }; + + self::assertValidInput(new Attributes($resolver), $input); + } + + #[Test] + public function shouldCreateAttributeValidatorsFromContainerWhenResolverGivenAndAttributeHasNoConstructor(): void + { + $resolver = new ContainerArgumentsResolver(new Container()); + + $input = new class { + #[AlwaysValid] + public string $property = 'value'; + }; + + self::assertValidInput(new Attributes($resolver), $input); + } + + #[Test] + public function shouldCreateAttributeValidatorsWithTheirDefaultsWithoutResolver(): void + { + $input = new class { + #[Injectable('some name')] + public string $property = 'value'; + }; + + self::assertInvalidInput(new Attributes(), $input); + } + /** @return array */ public static function providerForObjectsWithValidPropertyValues(): array { diff --git a/tests/unit/Validators/CountryCodeTest.php b/tests/unit/Validators/CountryCodeTest.php index ea3c6bcb7..47037a5ad 100644 --- a/tests/unit/Validators/CountryCodeTest.php +++ b/tests/unit/Validators/CountryCodeTest.php @@ -14,13 +14,10 @@ namespace Respect\Validation\Validators; -use DI; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; -use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Test\RuleTestCase; #[Group('validator')] @@ -39,24 +36,6 @@ public function itShouldThrowsExceptionWhenInvalidFormat(): void new CountryCode('whatever'); } - #[Test] - public function shouldThrowWhenMissingComponent(): void - { - $mainContainer = ContainerRegistry::getContainer(); - ContainerRegistry::setContainer((new DI\ContainerBuilder())->useAutowiring(false)->build()); - try { - new CountryCode('alpha-3'); - $this->fail('Expected MissingComposerDependencyException was not thrown.'); - } catch (MissingComposerDependencyException $e) { - $this->assertStringContainsString( - 'CountryCode rule requires PHP ISO Codes', - $e->getMessage(), - ); - } finally { - ContainerRegistry::setContainer($mainContainer); - } - } - /** @return iterable */ public static function providerForValidInput(): iterable { diff --git a/tests/unit/Validators/CurrencyCodeTest.php b/tests/unit/Validators/CurrencyCodeTest.php index fe2c0d56a..7de336f05 100644 --- a/tests/unit/Validators/CurrencyCodeTest.php +++ b/tests/unit/Validators/CurrencyCodeTest.php @@ -13,13 +13,10 @@ namespace Respect\Validation\Validators; -use DI; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; -use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Test\RuleTestCase; #[Group('validator')] @@ -38,24 +35,6 @@ public function itShouldThrowsExceptionWhenInvalidFormat(): void new CurrencyCode('whatever'); } - #[Test] - public function shouldThrowWhenMissingComponent(): void - { - $mainContainer = ContainerRegistry::getContainer(); - ContainerRegistry::setContainer((new DI\ContainerBuilder())->useAutowiring(false)->build()); - try { - new CurrencyCode('alpha-3'); - $this->fail('Expected MissingComposerDependencyException was not thrown.'); - } catch (MissingComposerDependencyException $e) { - $this->assertStringContainsString( - 'CurrencyCode rule requires PHP ISO Codes', - $e->getMessage(), - ); - } finally { - ContainerRegistry::setContainer($mainContainer); - } - } - /** @return iterable */ public static function providerForValidInput(): iterable { diff --git a/tests/unit/Validators/LanguageCodeTest.php b/tests/unit/Validators/LanguageCodeTest.php index 81b586b68..9f9527f6b 100644 --- a/tests/unit/Validators/LanguageCodeTest.php +++ b/tests/unit/Validators/LanguageCodeTest.php @@ -13,13 +13,10 @@ namespace Respect\Validation\Validators; -use DI; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; -use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Test\RuleTestCase; #[Group('validator')] @@ -38,24 +35,6 @@ public function itShouldThrowAnExceptionWhenSetIsInvalid(): void new LanguageCode('whatever'); } - #[Test] - public function shouldThrowWhenMissingComponent(): void - { - $mainContainer = ContainerRegistry::getContainer(); - ContainerRegistry::setContainer((new DI\ContainerBuilder())->useAutowiring(false)->build()); - try { - new LanguageCode('alpha-3'); - $this->fail('Expected MissingComposerDependencyException was not thrown.'); - } catch (MissingComposerDependencyException $e) { - $this->assertStringContainsString( - 'LanguageCode rule requires PHP ISO Codes', - $e->getMessage(), - ); - } finally { - ContainerRegistry::setContainer($mainContainer); - } - } - /** @return iterable */ public static function providerForValidInput(): iterable { diff --git a/tests/unit/Validators/PhoneTest.php b/tests/unit/Validators/PhoneTest.php index 745c1568c..609a54936 100644 --- a/tests/unit/Validators/PhoneTest.php +++ b/tests/unit/Validators/PhoneTest.php @@ -14,17 +14,12 @@ namespace Respect\Validation\Validators; -use DI; -use libphonenumber\PhoneNumberUtil; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; -use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Test\TestCase; -use Sokil\IsoCodes\Database\Countries; use stdClass; #[Group('validator')] @@ -68,56 +63,6 @@ public function itShouldThrowsExceptionWhenCountryCodeIsNotValid(): void new Phone('BRR'); } - #[Test] - public function shouldThrowWhenMissingIsocodesComponent(): void - { - $mainContainer = ContainerRegistry::getContainer(); - ContainerRegistry::setContainer( - (new DI\ContainerBuilder()) - ->addDefinitions([ - PhoneNumberUtil::class => DI\factory(static fn() => PhoneNumberUtil::getInstance()), - ]) - ->useAutowiring(false) - ->build(), - ); - try { - new Phone('US'); - $this->fail('Expected MissingComposerDependencyException was not thrown.'); - } catch (MissingComposerDependencyException $e) { - $this->assertStringContainsString( - 'Phone rule with country code requires PHP ISO Codes', - $e->getMessage(), - ); - } finally { - ContainerRegistry::setContainer($mainContainer); - } - } - - #[Test] - public function shouldThrowWhenMissingPhonesComponent(): void - { - $mainContainer = ContainerRegistry::getContainer(); - ContainerRegistry::setContainer( - (new DI\ContainerBuilder()) - ->addDefinitions([ - Countries::class => DI\create(Countries::class), - ]) - ->useAutowiring(false) - ->build(), - ); - try { - new Phone('US'); - $this->fail('Expected MissingComposerDependencyException was not thrown.'); - } catch (MissingComposerDependencyException $e) { - $this->assertStringContainsString( - 'Phone rule requires libphonenumber for PHP.', - $e->getMessage(), - ); - } finally { - ContainerRegistry::setContainer($mainContainer); - } - } - /** @return array */ public static function providerForValidInputWithoutCountryCode(): array { diff --git a/tests/unit/Validators/SubdivisionCodeTest.php b/tests/unit/Validators/SubdivisionCodeTest.php index 3490b868c..a21c72c06 100644 --- a/tests/unit/Validators/SubdivisionCodeTest.php +++ b/tests/unit/Validators/SubdivisionCodeTest.php @@ -11,13 +11,10 @@ namespace Respect\Validation\Validators; -use DI; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; -use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Test\RuleTestCase; #[Group('validator')] @@ -33,24 +30,6 @@ public function shouldNotAcceptWrongNamesOnConstructor(): void new SubdivisionCode('whatever'); } - #[Test] - public function shouldThrowWhenMissingComponent(): void - { - $mainContainer = ContainerRegistry::getContainer(); - ContainerRegistry::setContainer((new DI\ContainerBuilder())->useAutowiring(false)->build()); - try { - new SubdivisionCode('US'); - $this->fail('Expected MissingComposerDependencyException was not thrown.'); - } catch (MissingComposerDependencyException $e) { - $this->assertStringContainsString( - 'SubdivisionCode rule requires PHP ISO Codes', - $e->getMessage(), - ); - } finally { - ContainerRegistry::setContainer($mainContainer); - } - } - /** @return iterable */ public static function providerForValidInput(): iterable { diff --git a/tests/unit/Validators/UuidTest.php b/tests/unit/Validators/UuidTest.php index a1b384f9f..a14601c44 100644 --- a/tests/unit/Validators/UuidTest.php +++ b/tests/unit/Validators/UuidTest.php @@ -14,14 +14,11 @@ namespace Respect\Validation\Validators; -use DI; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Ramsey\Uuid\Uuid as RamseyUuid; -use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; -use Respect\Validation\Exceptions\MissingComposerDependencyException; use Respect\Validation\Test\RuleTestCase; use stdClass; @@ -70,24 +67,6 @@ public function itShouldThrowExceptionWhenVersionIsLessThanOne(): void new Uuid($version); } - #[Test] - public function shouldThrowWhenMissingComponent(): void - { - $mainContainer = ContainerRegistry::getContainer(); - ContainerRegistry::setContainer((new DI\ContainerBuilder())->useAutowiring(false)->build()); - try { - new Uuid(); - $this->fail('Expected MissingComposerDependencyException was not thrown.'); - } catch (MissingComposerDependencyException $e) { - $this->assertStringContainsString( - 'Uuid rule requires ramsey/uuid package', - $e->getMessage(), - ); - } finally { - ContainerRegistry::setContainer($mainContainer); - } - } - /** @return iterable */ public static function providerForValidInput(): iterable {