diff --git a/composer.json b/composer.json index 96b189202..c07cb11b5 100644 --- a/composer.json +++ b/composer.json @@ -21,9 +21,10 @@ }, "require": { "php": ">=8.5", - "php-di/php-di": "^7.1", "psr/container": "^2.0", + "respect/config": "^3.0", "respect/fluent": "^2.0", + "respect/parameter": "^3.0", "respect/string-formatter": "^1.7", "respect/stringifier": "^3.0", "symfony/polyfill-intl-idn": "^1.33", diff --git a/composer.lock b/composer.lock index 8baecc121..dd0c2d383 100644 --- a/composer.lock +++ b/composer.lock @@ -4,41 +4,34 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c007f19e671e2fec2eb7341be4146b75", + "content-hash": "683a4f0e3e7054eddcef31faf9cc9b1c", "packages": [ { - "name": "laravel/serializable-closure", - "version": "v2.0.13", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": "^8.1" - }, - "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0|^13.0", - "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0|^4.0", - "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + "php": ">=7.4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Laravel\\SerializableClosure\\": "src/" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -47,236 +40,169 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - }, - { - "name": "Nuno Maduro", - "email": "nuno@laravel.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "closure", - "laravel", - "serializable" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "issues": "https://github.com/laravel/serializable-closure/issues", - "source": "https://github.com/laravel/serializable-closure" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2026-04-16T14:03:50+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { - "name": "php-di/invoker", - "version": "2.3.7", + "name": "respect/config", + "version": "3.0.1", "source": { "type": "git", - "url": "https://github.com/PHP-DI/Invoker.git", - "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1" + "url": "https://github.com/Respect/Config.git", + "reference": "5dc7046d21e192083306e0472bf48e48d2968ff3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1", - "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "url": "https://api.github.com/repos/Respect/Config/zipball/5dc7046d21e192083306e0472bf48e48d2968ff3", + "reference": "5dc7046d21e192083306e0472bf48e48d2968ff3", "shasum": "" }, "require": { - "php": ">=7.3", - "psr/container": "^1.0|^2.0" + "php": ">=8.5", + "psr/container": "^2.0", + "respect/parameter": "^3.0" }, "require-dev": { - "athletic/athletic": "~0.1.8", - "mnapoli/hard-mode": "~0.3.0", - "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" + "mikey179/vfsstream": "^1.6", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^12.5 || ^13.0", + "respect/coding-standard": "^5.0", + "squizlabs/php_codesniffer": "^4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, "autoload": { "psr-4": { - "Invoker\\": "src/" + "Respect\\Config\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "description": "Generic and extensible callable invoker", - "homepage": "https://github.com/PHP-DI/Invoker", - "keywords": [ - "callable", - "dependency", - "dependency-injection", - "injection", - "invoke", - "invoker" + "BSD-3-Clause" ], - "support": { - "issues": "https://github.com/PHP-DI/Invoker/issues", - "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7" - }, - "funding": [ + "authors": [ { - "url": "https://github.com/mnapoli", - "type": "github" + "name": "Respect/Config Contributors", + "homepage": "https://github.com/Respect/Config/graphs/contributors" } ], - "time": "2025-08-30T10:22:22+00:00" - }, - { - "name": "php-di/php-di", - "version": "7.1.1", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "f88054cc052e40dbe7b383c8817c19442d480352" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", - "reference": "f88054cc052e40dbe7b383c8817c19442d480352", - "shasum": "" - }, - "require": { - "laravel/serializable-closure": "^1.0 || ^2.0", - "php": ">=8.0", - "php-di/invoker": "^2.0", - "psr/container": "^1.1 || ^2.0" - }, - "provide": { - "psr/container-implementation": "^1.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3", - "friendsofphp/proxy-manager-lts": "^1", - "mnapoli/phpunit-easymock": "^1.3", - "phpunit/phpunit": "^9.6 || ^10 || ^11", - "vimeo/psalm": "^5|^6" - }, - "suggest": { - "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "DI\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The dependency injection container for humans", - "homepage": "https://php-di.org/", + "description": "A powerful, small, deadly simple configurator and dependency injection container made to be easy.", + "homepage": "https://github.com/Respect/Config", "keywords": [ - "PSR-11", - "container", - "container-interop", + "config", "dependency injection", - "di", - "ioc", - "psr11" + "dic", + "respect" ], "support": { - "issues": "https://github.com/PHP-DI/PHP-DI/issues", - "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" + "issues": "https://github.com/Respect/Config/issues", + "source": "https://github.com/Respect/Config/tree/3.0.1" }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", - "type": "tidelift" - } - ], - "time": "2025-08-16T11:10:48+00:00" + "time": "2026-06-25T22:41:57+00:00" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "respect/fluent", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/Respect/Fluent.git", + "reference": "f32c76e37a82a9e63d6fe700a27201534f72da60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/Respect/Fluent/zipball/f32c76e37a82a9e63d6fe700a27201534f72da60", + "reference": "f32c76e37a82a9e63d6fe700a27201534f72da60", "shasum": "" }, "require": { - "php": ">=7.4.0" + "php": "^8.5" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^12.5", + "respect/coding-standard": "^5.0" }, + "type": "library", "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "Respect\\Fluent\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "ISC" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Respect/Fluent Contributors", + "homepage": "https://github.com/Respect/Fluent/graphs/contributors" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Namespace-aware fluent class resolution", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "builder", + "fluent", + "respect" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "issues": "https://github.com/Respect/Fluent/issues", + "source": "https://github.com/Respect/Fluent/tree/2.0.1" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2026-03-26T04:24:51+00:00" }, { - "name": "respect/fluent", - "version": "2.0.1", + "name": "respect/parameter", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/Respect/Fluent.git", - "reference": "f32c76e37a82a9e63d6fe700a27201534f72da60" + "url": "https://github.com/Respect/Parameter.git", + "reference": "5dd531c196113bc3d12d2ada1f966193e921744b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Respect/Fluent/zipball/f32c76e37a82a9e63d6fe700a27201534f72da60", - "reference": "f32c76e37a82a9e63d6fe700a27201534f72da60", + "url": "https://api.github.com/repos/Respect/Parameter/zipball/5dd531c196113bc3d12d2ada1f966193e921744b", + "reference": "5dd531c196113bc3d12d2ada1f966193e921744b", "shasum": "" }, "require": { - "php": "^8.5" + "php": "^8.5", + "psr/container": "^2.0" }, "require-dev": { "phpstan/phpstan": "^2.1", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^12.5", + "phpunit/phpunit": "^12.5 || ^13.0", "respect/coding-standard": "^5.0" }, "type": "library", "autoload": { "psr-4": { - "Respect\\Fluent\\": "src/" + "Respect\\Parameter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -285,21 +211,24 @@ ], "authors": [ { - "name": "Respect/Fluent Contributors", - "homepage": "https://github.com/Respect/Fluent/graphs/contributors" + "name": "Respect/Parameter Contributors", + "homepage": "https://github.com/Respect/Parameter/graphs/contributors" } ], - "description": "Namespace-aware fluent class resolution", + "description": "Resolves function/constructor parameters via type and name lookup", "keywords": [ - "builder", - "fluent", + "Autowiring", + "PSR-11", + "dependency-injection", + "parameter", + "resolver", "respect" ], "support": { - "issues": "https://github.com/Respect/Fluent/issues", - "source": "https://github.com/Respect/Fluent/tree/2.0.1" + "issues": "https://github.com/Respect/Parameter/issues", + "source": "https://github.com/Respect/Parameter/tree/3.0.0" }, - "time": "2026-03-26T04:24:51+00:00" + "time": "2026-06-25T22:35:05+00:00" }, { "name": "respect/string-formatter", @@ -5164,16 +5093,16 @@ }, { "name": "seld/jsonlint", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "d95c42df9c4a713ca1d4a45b37f0416dee1ea73a" + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d95c42df9c4a713ca1d4a45b37f0416dee1ea73a", - "reference": "d95c42df9c4a713ca1d4a45b37f0416dee1ea73a", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", "shasum": "" }, "require": { @@ -5212,7 +5141,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.12.0" + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" }, "funding": [ { @@ -5224,7 +5153,7 @@ "type": "tidelift" } ], - "time": "2026-06-11T13:43:55+00:00" + "time": "2026-06-12T11:32:29+00:00" }, { "name": "slevomat/coding-standard", @@ -5989,16 +5918,16 @@ }, { "name": "symfony/polyfill-deepclone", - "version": "v1.39.0", + "version": "v1.40.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-deepclone.git", - "reference": "1b034bc050d84cc9c187de373f744912e1e35f1f" + "reference": "dca4ccba5f360070b574414dce4c1e7a559844fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/1b034bc050d84cc9c187de373f744912e1e35f1f", - "reference": "1b034bc050d84cc9c187de373f744912e1e35f1f", + "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/dca4ccba5f360070b574414dce4c1e7a559844fa", + "reference": "dca4ccba5f360070b574414dce4c1e7a559844fa", "shasum": "" }, "require": { @@ -6052,7 +5981,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.39.0" + "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.40.0" }, "funding": [ { @@ -6072,7 +6001,7 @@ "type": "tidelift" } ], - "time": "2026-06-10T20:07:50+00:00" + "time": "2026-06-12T07:27:17+00:00" }, { "name": "symfony/polyfill-intl-grapheme", diff --git a/docs/configuration.md b/docs/configuration.md index aaa3d818c..cfd9c59b7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,9 +8,9 @@ SPDX-FileContributor: Alexandre Gomes Gaigalas ## Container configuration -The `ContainerRegistry::createContainer()` method returns a [PHP-DI](https://php-di.org/) container. The definitions array follows the [PHP-DI definitions format](https://php-di.org/doc/php-definitions.html). +The `ContainerRegistry::createContainer()` method returns a [Respect\Config](https://github.com/Respect/Config) container, which is [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible. Definitions may be plain values, closures, or Respect\Config's `Autowire`, `Instantiator`, and `Ref` helpers. -If you prefer to use a different container, `ContainerRegistry::setContainer()` accepts any [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container: +If you prefer to use a different container, `ContainerRegistry::setContainer()` accepts any PSR-11 compatible container: ```php use Respect\Validation\ContainerRegistry; diff --git a/docs/messages/placeholder-conversion.md b/docs/messages/placeholder-conversion.md index bb11fee4f..73e10b02e 100644 --- a/docs/messages/placeholder-conversion.md +++ b/docs/messages/placeholder-conversion.md @@ -16,20 +16,16 @@ You can add custom modifiers by providing a custom `PlaceholderFormatter` to the `ContainerRegistry`: ```php -use DI\Container; +use Respect\Config\Container; use Respect\StringFormatter\Modifier; use Respect\StringFormatter\PlaceholderFormatter; use Respect\Validation\ContainerRegistry; -use function DI\factory; - ContainerRegistry::setContainer( ContainerRegistry::createContainer([ - PlaceholderFormatter::class => factory( - fn(Container $container) => new PlaceholderFormatter( - [], - new MyCustomModifier($container->get(Modifier::class)), - ), + PlaceholderFormatter::class => static fn(Container $container) => new PlaceholderFormatter( + [], + new MyCustomModifier($container->get(Modifier::class)), ), ]) ); diff --git a/docs/migrating-from-v2-to-v3.md b/docs/migrating-from-v2-to-v3.md index 3ac05e504..385827273 100644 --- a/docs/migrating-from-v2-to-v3.md +++ b/docs/migrating-from-v2-to-v3.md @@ -504,7 +504,7 @@ The `Factory` class has been replaced by a dependency injection container approa + ContainerRegistry::setContainer($container); ``` -The `ContainerRegistry::createContainer()` returns a [PHP-DI](https://php-di.org/) container. You can also use any PSR-11 compatible container with `ContainerRegistry::setContainer()`. +The `ContainerRegistry::createContainer()` returns a [Respect\Config](https://github.com/Respect/Config) container, which is [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible. You can also use any PSR-11 compatible container with `ContainerRegistry::setContainer()`. ### Custom validators diff --git a/docs/validators/Attributes.md b/docs/validators/Attributes.md index f518198ca..c338af1a9 100644 --- a/docs/validators/Attributes.md +++ b/docs/validators/Attributes.md @@ -8,6 +8,7 @@ SPDX-FileContributor: Henrique Moody # Attributes - `Attributes()` +- `Attributes(Resolver $resolver)` Validates the PHP attributes defined in the properties of the input. diff --git a/docs/validators/Uuid.md b/docs/validators/Uuid.md index bba103ac5..6c726cfd1 100644 --- a/docs/validators/Uuid.md +++ b/docs/validators/Uuid.md @@ -12,6 +12,7 @@ SPDX-FileContributor: steven.lewis - `Uuid()` - `Uuid(int $version)` +- `Uuid(int $version, UuidFactory $uuidFactory)` Validates whether the input is a valid UUID. It also supports validation of specific versions 1 to 8. diff --git a/src-dev/Commands/LintMixinCommand.php b/src-dev/Commands/LintMixinCommand.php index c9fb1db2d..a31f6b188 100644 --- a/src-dev/Commands/LintMixinCommand.php +++ b/src-dev/Commands/LintMixinCommand.php @@ -74,8 +74,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int config: $config, scanner: $scanner, methodBuilder: new MethodBuilder( - excludedTypePrefixes: ['Sokil', 'Egulias'], - excludedTypeNames: ['finfo'], + excludedTypePrefixes: ['Sokil', 'Egulias', 'Ramsey', 'libphonenumber'], + excludedTypeNames: ['Respect\\Parameter\\Resolver'], ), interfaces: [ new InterfaceConfig( diff --git a/src/AutowiringLookup.php b/src/AutowiringLookup.php new file mode 100644 index 000000000..fce97473b --- /dev/null +++ b/src/AutowiringLookup.php @@ -0,0 +1,72 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use Respect\Fluent\Exceptions\CouldNotCreate; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\FluentFactory; +use Respect\Fluent\FluentNode; +use Respect\Fluent\FluentResolver; +use Respect\Parameter\Resolver; +use Throwable; + +use function sprintf; + +final readonly class AutowiringLookup implements FluentFactory +{ + public function __construct( + private NamespaceLookup $lookup, + private FluentResolver $resolver, + private Resolver $parameterResolver, + ) { + } + + public function withNamespace(string $namespace): static + { + return clone ($this, ['lookup' => $this->lookup->withNamespace($namespace)]); + } + + /** @param array $arguments */ + public function create(string $name, array $arguments = []): object + { + $spec = $this->resolver->resolve(new FluentNode($name, $arguments)); + + $instance = $this->instantiate($spec->name, $spec->arguments); + + $wrapper = $spec->wrapper; + while ($wrapper !== null) { + $instance = $this->instantiate($wrapper->name, [...$wrapper->arguments, $instance]); + $wrapper = $wrapper->wrapper; + } + + return $instance; + } + + /** @param array $arguments */ + private function instantiate(string $name, array $arguments): object + { + $reflection = $this->lookup->resolve($name); + + $constructor = $reflection->getConstructor(); + try { + if ($constructor === null) { + return $reflection->newInstanceArgs($arguments); + } + + return $reflection->newInstanceArgs($this->parameterResolver->resolve($constructor, $arguments)); + } catch (Throwable $exception) { + throw new CouldNotCreate( + sprintf('Could not instantiate "%s": %s', $name, $exception->getMessage()), + previous: $exception, + ); + } + } +} diff --git a/src/ContainerRegistry.php b/src/ContainerRegistry.php index 5eb32bc0a..8f2820202 100644 --- a/src/ContainerRegistry.php +++ b/src/ContainerRegistry.php @@ -11,13 +11,17 @@ namespace Respect\Validation; -use DI\Container; use libphonenumber\PhoneNumberUtil; use Psr\Container\ContainerInterface; -use Respect\Fluent\Factories\ComposingLookup; +use Ramsey\Uuid\UuidFactory; +use Respect\Config\Autowire; +use Respect\Config\Container; +use Respect\Config\Instantiator; use Respect\Fluent\Factories\NamespaceLookup; use Respect\Fluent\Resolvers\ComposableMap; use Respect\Fluent\Resolvers\Ucfirst; +use Respect\Parameter\ContainerResolver; +use Respect\Parameter\Resolver; use Respect\StringFormatter\BypassTranslator; use Respect\StringFormatter\Modifier; use Respect\StringFormatter\Modifiers\FormatterModifier; @@ -45,12 +49,12 @@ use Respect\Validation\Message\Renderer; use Respect\Validation\Message\TemplateRegistry; use Respect\Validation\Mixins\PrefixConstants; +use Sokil\IsoCodes\Database\Countries; +use Sokil\IsoCodes\Database\Currencies; +use Sokil\IsoCodes\Database\Languages; +use Sokil\IsoCodes\Database\Subdivisions; use Symfony\Contracts\Translation\TranslatorInterface; -use function DI\autowire; -use function DI\create; -use function DI\factory; - final class ContainerRegistry { private static ContainerInterface|null $container = null; @@ -59,50 +63,56 @@ final class ContainerRegistry public static function createContainer(array $definitions = []): Container { return new Container($definitions + [ - PhoneNumberUtil::class => factory(static fn() => PhoneNumberUtil::getInstance()), - TemplateRegistry::class => create(TemplateRegistry::class), - TemplateResolver::class => autowire(TemplateResolver::class), - TranslatorInterface::class => autowire(BypassTranslator::class), - Renderer::class => autowire(InterpolationRenderer::class), - ResultFilter::class => create(OnlyFailedChildrenResultFilter::class), - 'respect.validation.formatter.message' => autowire(FirstResultStringFormatter::class), - 'respect.validation.formatter.full_message' => autowire(NestedListStringFormatter::class), - 'respect.validation.formatter.messages' => autowire(NestedArrayFormatter::class), + Countries::class => new Instantiator(Countries::class), + Currencies::class => new Instantiator(Currencies::class), + Languages::class => new Instantiator(Languages::class), + Subdivisions::class => new Instantiator(Subdivisions::class), + UuidFactory::class => new Instantiator(UuidFactory::class), + PhoneNumberUtil::class => static fn() => PhoneNumberUtil::getInstance(), + TemplateRegistry::class => new Instantiator(TemplateRegistry::class), + TemplateResolver::class => new Autowire(TemplateResolver::class), + TranslatorInterface::class => new Autowire(BypassTranslator::class), + Renderer::class => new Autowire(InterpolationRenderer::class), + ResultFilter::class => new Instantiator(OnlyFailedChildrenResultFilter::class), + 'respect.validation.formatter.message' => new Autowire(FirstResultStringFormatter::class), + 'respect.validation.formatter.full_message' => new Autowire(NestedListStringFormatter::class), + 'respect.validation.formatter.messages' => new Autowire(NestedArrayFormatter::class), 'respect.validation.ignored_backtrace_paths' => [__DIR__ . '/ValidatorBuilder.php'], 'respect.validation.rule_factory.namespaces' => ['Respect\\Validation\\Validators'], - ValidatorFactory::class => factory(static function (Container $container) { + Resolver::class => static fn(Container $container) => new ContainerResolver($container), + ValidatorFactory::class => static function (Container $container) { + $lookup = new NamespaceLookup( + new Ucfirst(), + Validator::class, + ...$container->get('respect.validation.rule_factory.namespaces'), + ); + return new FluentValidatorFactory( - new ComposingLookup( - new NamespaceLookup( - new Ucfirst(), - Validator::class, - ...$container->get('respect.validation.rule_factory.namespaces'), - ), - new ComposableMap( - PrefixConstants::COMPOSABLE, - PrefixConstants::COMPOSABLE_WITH_ARGUMENT, - ), + new AutowiringLookup( + $lookup, + new ComposableMap(PrefixConstants::COMPOSABLE, PrefixConstants::COMPOSABLE_WITH_ARGUMENT), + $container->get(Resolver::class), ), ); - }), - Quoter::class => create(CodeQuoter::class)->constructor(120), - Handler::class => factory(static function (Container $container) { + }, + Quoter::class => new Instantiator(CodeQuoter::class, ['maximumLength' => 120]), + Handler::class => static function (Container $container) { $handler = CompositeHandler::create(); $handler->prependHandler(new PathHandler($container->get(Quoter::class))); $handler->prependHandler(new NameHandler()); $handler->prependHandler(new ResultHandler($handler)); return $handler; - }), - PlaceholderFormatter::class => factory(static fn(Container $container) => new PlaceholderFormatter( + }, + PlaceholderFormatter::class => static fn(Container $container) => new PlaceholderFormatter( [], $container->get(Modifier::class), - )), - Stringifier::class => factory(static fn(Container $container) => new HandlerStringifier( + ), + Stringifier::class => static fn(Container $container) => new HandlerStringifier( $container->get(Handler::class), new DumpStringifier(), - )), - Modifier::class => factory(static fn(Container $container) => new TransModifier( + ), + Modifier::class => static fn(Container $container) => new TransModifier( new ListModifier( new QuoteModifier( new RawModifier( @@ -112,8 +122,8 @@ public static function createContainer(array $definitions = []): Container $container->get(TranslatorInterface::class), ), $container->get(TranslatorInterface::class), - )), - ValidatorBuilder::class => factory(static fn(Container $container) => new ValidatorBuilder( + ), + ValidatorBuilder::class => static fn(Container $container) => new ValidatorBuilder( $container->get(ValidatorFactory::class), $container->get(Renderer::class), $container->get('respect.validation.formatter.message'), @@ -121,7 +131,7 @@ public static function createContainer(array $definitions = []): Container $container->get('respect.validation.formatter.messages'), $container->get(ResultFilter::class), $container->get('respect.validation.ignored_backtrace_paths'), - )), + ), ]); } diff --git a/src/NamespacedValidatorFactory.php b/src/NamespacedValidatorFactory.php index 369dd8994..5aa1230a4 100644 --- a/src/NamespacedValidatorFactory.php +++ b/src/NamespacedValidatorFactory.php @@ -24,6 +24,11 @@ use function trim; use function ucfirst; +/** + * @deprecated This has been superseeded by Respect\Fluent + * + * @see \Respect\Validation\FluentValidatorFactory + */ final readonly class NamespacedValidatorFactory implements ValidatorFactory { /** @param array $rulesNamespaces */ diff --git a/src/Validators/Attributes.php b/src/Validators/Attributes.php index d0fd303f8..9e277772f 100644 --- a/src/Validators/Attributes.php +++ b/src/Validators/Attributes.php @@ -21,6 +21,7 @@ use ReflectionProperty; use ReflectionUnionType; use Respect\Fluent\Attributes\Composable; +use Respect\Parameter\Resolver; use Respect\Validation\Id; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -43,6 +44,10 @@ final class Attributes implements Validator /** @var array */ private array $visited = []; + public function __construct(private readonly Resolver|null $resolver = null) + { + } + public function evaluate(mixed $input): Result { $id = new Id('attributes'); @@ -73,7 +78,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->instantiateAttribute($attribute); } $reflection = $reflection->getParentClass(); @@ -107,8 +112,13 @@ private function getPropertyInnerValidators(ReflectionProperty $property): array $propertyValidators = []; $hasExplicitAttributes = false; foreach ($property->getAttributes(Validator::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $propertyValidator = $attribute->getName() === self::class ? $this : $attribute->newInstance(); - $hasExplicitAttributes = $propertyValidator === $this; + if ($attribute->getName() === self::class) { + $propertyValidator = $this; + } else { + $propertyValidator = $this->instantiateAttribute($attribute); + } + + $hasExplicitAttributes = $hasExplicitAttributes || $propertyValidator === $this; $propertyValidators[] = $propertyValidator; } @@ -156,4 +166,20 @@ private function getProperties(ReflectionObject $reflection): array return $properties; } + + /** @param ReflectionAttribute $attribute */ + private function instantiateAttribute(ReflectionAttribute $attribute): Validator + { + if ($this->resolver === null) { + return $attribute->newInstance(); + } + + $reflection = new ReflectionClass($attribute->getName()); + $constructor = $reflection->getConstructor(); + if ($constructor === null) { + return $attribute->newInstance(); + } + + return $reflection->newInstanceArgs($this->resolver->resolve($constructor, $attribute->getArguments())); + } } 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/Email.php b/src/Validators/Email.php index 5877204e5..86f52296e 100644 --- a/src/Validators/Email.php +++ b/src/Validators/Email.php @@ -26,7 +26,6 @@ use function class_exists; use function filter_var; -use function func_num_args; use function is_string; use const FILTER_VALIDATE_EMAIL; @@ -42,7 +41,7 @@ final class Email extends Simple public function __construct(EmailValidator|null $validator = null) { - if ($validator === null && func_num_args() === 0 && class_exists(EmailValidator::class)) { + if ($validator === null && class_exists(EmailValidator::class)) { $validator = new EmailValidator(); } 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..7778c2339 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,9 +49,11 @@ 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)) { + public function __construct( + string|null $countryCode = null, + Countries|null $countries = null, + ) { + if (!class_exists(PhoneNumberUtil::class)) { throw new MissingComposerDependencyException( 'Phone rule requires libphonenumber for PHP', 'giggsey/libphonenumber-for-php', @@ -65,9 +66,7 @@ public function __construct(string|null $countryCode = null, Countries|null $cou 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,6 +74,8 @@ public function __construct(string|null $countryCode = null, Countries|null $cou ); } + $countries ??= new Countries(); + $this->country = $countries->getByAlpha2($countryCode); if ($this->country === null) { throw new InvalidValidatorException('Invalid country code %s', $countryCode); @@ -95,7 +96,7 @@ public function evaluate(mixed $input): Result private function isValidPhone(string $input): bool { try { - $phoneNumberUtil = ContainerRegistry::getContainer()->get(PhoneNumberUtil::class); + $phoneNumberUtil = PhoneNumberUtil::getInstance(); $phoneNumberObject = $phoneNumberUtil->parse($input, $this->country?->getAlpha2()); if ($this->country === null) { return $phoneNumberUtil->isValidNumber($phoneNumberObject); diff --git a/src/Validators/SubdivisionCode.php b/src/Validators/SubdivisionCode.php index da3c721f1..939ca22a2 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,10 @@ 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 && !class_exists(Countries::class)) + || ($subdivisions === null && !class_exists(Subdivisions::class)) + ) { throw new MissingComposerDependencyException( 'SubdivisionCode rule requires PHP ISO Codes', 'sokil/php-isocodes', @@ -53,6 +52,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/Stubs/WithAttributesNotLastOnNested.php b/tests/src/Stubs/WithAttributesNotLastOnNested.php new file mode 100644 index 000000000..92df177a0 --- /dev/null +++ b/tests/src/Stubs/WithAttributesNotLastOnNested.php @@ -0,0 +1,23 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Test\Stubs; + +use Respect\Validation\Validators as Rule; + +final class WithAttributesNotLastOnNested +{ + public function __construct( + #[Rule\Attributes] + #[Rule\Instance(NestedAddress::class)] + public NestedAddress $address, + ) { + } +} diff --git a/tests/src/Validators/WithDependency.php b/tests/src/Validators/WithDependency.php new file mode 100644 index 000000000..720693fdb --- /dev/null +++ b/tests/src/Validators/WithDependency.php @@ -0,0 +1,26 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Test\Validators; + +use Respect\Validation\Validators\Core\Simple; +use stdClass; + +final class WithDependency extends Simple +{ + public function __construct(public readonly stdClass $dependency) + { + } + + public function isValid(mixed $input): bool + { + return true; + } +} diff --git a/tests/unit/AutowiringLookupTest.php b/tests/unit/AutowiringLookupTest.php new file mode 100644 index 000000000..39fa50f31 --- /dev/null +++ b/tests/unit/AutowiringLookupTest.php @@ -0,0 +1,99 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use Respect\Config\Container; +use Respect\Fluent\Exceptions\CouldNotCreate; +use Respect\Fluent\Exceptions\CouldNotResolve; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\ComposableMap; +use Respect\Fluent\Resolvers\Ucfirst; +use Respect\Parameter\ContainerResolver; +use Respect\Validation\Mixins\PrefixConstants; +use Respect\Validation\Test\TestCase; +use Respect\Validation\Test\Validators\Stub; +use Respect\Validation\Test\Validators\Valid; +use Respect\Validation\Test\Validators\WithDependency; +use stdClass; + +use function assert; + +#[Group('core')] +#[CoversClass(AutowiringLookup::class)] +final class AutowiringLookupTest extends TestCase +{ + private const string TEST_RULES_NAMESPACE = 'Respect\\Validation\\Test\\Validators'; + + #[Test] + public function itShouldCreateRuleByNameFromNamespace(): void + { + self::assertInstanceOf(Valid::class, $this->createLookup()->create('valid')); + } + + #[Test] + public function itShouldPrependNamespaceWithWithNamespace(): void + { + $lookup = $this->createLookup()->withNamespace(__NAMESPACE__); + + self::assertInstanceOf(Valid::class, $lookup->create('valid')); + } + + #[Test] + public function itShouldPassAllArgumentsToVariadicConstructors(): void + { + $arguments = [true, false, true, false]; + + $rule = $this->createLookup()->create('stub', $arguments); + assert($rule instanceof Stub); + + self::assertSame($arguments, $rule->validations); + } + + #[Test] + public function itShouldAutowireConstructorDependenciesFromTheContainer(): void + { + $dependency = new stdClass(); + + $rule = $this->createLookup([stdClass::class => $dependency])->create('withDependency'); + assert($rule instanceof WithDependency); + + self::assertSame($dependency, $rule->dependency); + } + + #[Test] + public function itShouldThrowWhenRuleNameCannotBeResolved(): void + { + $this->expectException(CouldNotResolve::class); + + $this->createLookup()->create('nonExistingRule'); + } + + #[Test] + public function itShouldThrowWhenInstantiationFails(): void + { + $this->expectException(CouldNotCreate::class); + + $this->createLookup()->create('noConstructor', ['a', 'b']); + } + + /** @param array $definitions */ + private function createLookup(array $definitions = []): AutowiringLookup + { + return new AutowiringLookup( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_RULES_NAMESPACE), + new ComposableMap(PrefixConstants::COMPOSABLE, PrefixConstants::COMPOSABLE_WITH_ARGUMENT), + new ContainerResolver(new Container($definitions)), + ); + } +} diff --git a/tests/unit/NamespacedRuleFactoryTest.php b/tests/unit/NamespacedRuleFactoryTest.php deleted file mode 100644 index 3dc428190..000000000 --- a/tests/unit/NamespacedRuleFactoryTest.php +++ /dev/null @@ -1,110 +0,0 @@ - - * SPDX-FileContributor: Augusto Pascutti - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\Exceptions\ComponentException; -use Respect\Validation\Exceptions\InvalidClassException; -use Respect\Validation\Test\TestCase; -use Respect\Validation\Test\Transformers\StubTransformer; -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 function assert; -use function sprintf; - -#[Group('core')] -#[CoversClass(NamespacedValidatorFactory::class)] -final class NamespacedRuleFactoryTest extends TestCase -{ - private const string TEST_RULES_NAMESPACE = 'Respect\\Validation\\Test\\Validators'; - - #[Test] - public function shouldCreateRuleByNameBasedOnNamespace(): void - { - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - self::assertInstanceOf(Valid::class, $factory->create('valid')); - } - - #[Test] - public function shouldLookUpToAllNamespacesUntilRuleIsFound(): void - { - $factory = (new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE])) - ->withNamespace(__NAMESPACE__); - - self::assertInstanceOf(Valid::class, $factory->create('valid')); - } - - #[Test] - public function shouldDefineConstructorArgumentsWhenCreatingRule(): void - { - $constructorArguments = [true, false, true, false]; - - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - $validator = $factory->create('stub', $constructorArguments); - assert($validator instanceof Stub); - - self::assertSame($constructorArguments, $validator->validations); - } - - #[Test] - public function shouldThrowsAnExceptionOnConstructorReflectionFailure(): void - { - $constructorArguments = ['a', 'b']; - - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - $this->expectException(InvalidClassException::class); - $this->expectExceptionMessage('"noConstructor" could not be instantiated with arguments `["a", "b"]`'); - - $factory->create('noConstructor', $constructorArguments); - } - - #[Test] - public function shouldThrowsAnExceptionWhenRuleIsInvalid(): void - { - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - $this->expectException(InvalidClassException::class); - $this->expectExceptionMessage(sprintf('"%s" must be an instance of "%s"', Invalid::class, Validator::class)); - - $factory->create('invalid'); - } - - #[Test] - public function shouldThrowsAnExceptionWhenRuleIsNotInstantiable(): void - { - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - $this->expectException(InvalidClassException::class); - $this->expectExceptionMessage(sprintf('"%s" must be instantiable', MyAbstractClass::class)); - - $factory->create('myAbstractClass'); - } - - #[Test] - public function shouldThrowsAnExceptionWhenRuleIsNotFound(): void - { - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - $this->expectException(ComponentException::class); - $this->expectExceptionMessage('"nonExistingRule" is not a valid rule name'); - - $factory->create('nonExistingRule'); - } -} diff --git a/tests/unit/Validators/AttributesTest.php b/tests/unit/Validators/AttributesTest.php index e5c1b9035..c44876e77 100644 --- a/tests/unit/Validators/AttributesTest.php +++ b/tests/unit/Validators/AttributesTest.php @@ -22,6 +22,7 @@ use Respect\Validation\Test\Stubs\NestedWithAttributes; use Respect\Validation\Test\Stubs\NestedWithoutAttributes; use Respect\Validation\Test\Stubs\WithAttributes; +use Respect\Validation\Test\Stubs\WithAttributesNotLastOnNested; use Respect\Validation\Test\Stubs\WithCyclicAttributes; use Respect\Validation\Test\Stubs\WithDeeplyNestedAttributes; use Respect\Validation\Test\Stubs\WithExplicitAttributesOnNested; @@ -240,6 +241,14 @@ public function shouldValidateNestedObjectWithExplicitAttributesWhenInvalid(): v self::assertInvalidInput(new Attributes(), $input); } + #[Test] + public function shouldNotDuplicateAttributesWhenNotTheLastAttributeOnNestedProperty(): void + { + $input = new WithAttributesNotLastOnNested(new NestedAddress('123 Main St', 'Springfield')); + + self::assertValidInput(new Attributes(), $input); + } + #[Test] public function shouldRejectSelfReferencingCyclicObjectGraph(): void { 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 {