From 7d39cc2f853cee99e1d8d94da6d7b631d0d68183 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Thu, 25 Jun 2026 08:11:12 -0300 Subject: [PATCH] Integrate Respect/Config and Respect/Parameter Replace the PHP-DI container with Respect/Config: ContainerRegistry now builds a Respect\Config\Container, registering the rules' external dependencies (PHP ISO Codes, libphonenumber, ramsey/uuid) and a ParameterResolver. Instantiate rules through a new AutowiringLookup: a Respect\Fluent FluentFactory that resolves the rule name and autowires its constructor dependencies from the container via Respect\Parameter, wrapped by FluentValidatorFactory. The dead NamespacedValidatorFactory is removed. Drop static ContainerRegistry access from CountryCode, CurrencyCode, LanguageCode, SubdivisionCode, Phone and Uuid: each accepts its external dependency and otherwise constructs it directly, guarded by class_exists for the missing-package error. Inject a ParameterResolver into Attributes so attribute-declared rules are autowired too. Fix Email: drop the func_num_args() guard, which autowiring defeats (the resolver always passes the defaulted argument), so v::email() uses egulias again instead of silently falling back to filter_var. Exclude the autowired dependency types from the generated mixins, update the docs, and add AutowiringLookupTest. --- composer.json | 3 +- composer.lock | 297 +++++++----------- docs/configuration.md | 4 +- docs/messages/placeholder-conversion.md | 12 +- docs/migrating-from-v2-to-v3.md | 2 +- docs/validators/Attributes.md | 1 + docs/validators/Uuid.md | 1 + src-dev/Commands/LintMixinCommand.php | 4 +- src/AutowiringLookup.php | 72 +++++ src/ContainerRegistry.php | 86 ++--- src/NamespacedValidatorFactory.php | 5 + src/Validators/Attributes.php | 32 +- src/Validators/CountryCode.php | 9 +- src/Validators/CurrencyCode.php | 9 +- src/Validators/Email.php | 3 +- src/Validators/LanguageCode.php | 9 +- src/Validators/Phone.php | 19 +- src/Validators/SubdivisionCode.php | 16 +- src/Validators/Uuid.php | 13 +- .../Stubs/WithAttributesNotLastOnNested.php | 23 ++ tests/src/Validators/WithDependency.php | 26 ++ tests/unit/AutowiringLookupTest.php | 99 ++++++ tests/unit/NamespacedRuleFactoryTest.php | 110 ------- tests/unit/Validators/AttributesTest.php | 9 + 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 -- 30 files changed, 477 insertions(+), 547 deletions(-) create mode 100644 src/AutowiringLookup.php create mode 100644 tests/src/Stubs/WithAttributesNotLastOnNested.php create mode 100644 tests/src/Validators/WithDependency.php create mode 100644 tests/unit/AutowiringLookupTest.php delete mode 100644 tests/unit/NamespacedRuleFactoryTest.php 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 {