From 5c982d2f2c94234423697a7cf420daa4a2311e02 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Mon, 23 Mar 2026 19:08:15 -0300 Subject: [PATCH] Integrate Respect/Fluent and Respect/FluentGen Replace the in-house factory and codegen infrastructure with Fluent and FluentGen packages. The runtime now uses ComposingLookup with ComposableMap for prefix resolution via a FluentValidatorFactory adapter that preserves exception types. Validators use #[Composable] from Fluent instead of the custom #[Mixin] attribute. Mixin generation uses FluentGen, producing PrefixConstants in place of PrefixMap. The old src-dev/CodeGen directory is removed. All public APIs and BC are preserved. --- composer.json | 2 + composer.lock | 261 +++++++++++++----- src-dev/CodeGen/Config.php | 23 -- .../CodeGen/FluentBuilder/MethodBuilder.php | 169 ------------ src-dev/CodeGen/FluentBuilder/Mixin.php | 30 -- .../CodeGen/FluentBuilder/MixinGenerator.php | 253 ----------------- .../FluentBuilder/PrefixMapGenerator.php | 160 ----------- src-dev/CodeGen/InterfaceConfig.php | 28 -- src-dev/CodeGen/NamespaceScanner.php | 44 --- src-dev/CodeGen/OutputFormatter.php | 47 ---- src-dev/Commands/LintMixinCommand.php | 19 +- src/ContainerRegistry.php | 34 ++- src/FluentValidatorFactory.php | 53 ++++ src/Mixins/Builder.php | 6 +- src/Mixins/Chain.php | 6 +- src/Mixins/NotBuilder.php | 6 +- src/Mixins/NotChain.php | 6 +- src/Mixins/NullOrBuilder.php | 6 +- src/Mixins/NullOrChain.php | 6 +- src/Mixins/PrefixConstants.php | 56 ++++ src/Mixins/PrefixMap.php | 33 --- src/Mixins/UndefOrBuilder.php | 6 +- src/Mixins/UndefOrChain.php | 6 +- src/Transformers/Prefix.php | 8 +- src/ValidatorBuilder.php | 39 ++- src/Validators/All.php | 4 +- src/Validators/Attributes.php | 4 +- src/Validators/Between.php | 4 +- src/Validators/BetweenExclusive.php | 4 +- src/Validators/Blank.php | 4 +- src/Validators/Equals.php | 4 +- src/Validators/Equivalent.php | 4 +- src/Validators/Even.php | 4 +- src/Validators/Exists.php | 4 +- src/Validators/Factor.php | 4 +- src/Validators/Finite.php | 4 +- src/Validators/Formatted.php | 4 +- src/Validators/GreaterThan.php | 4 +- src/Validators/GreaterThanOrEqual.php | 4 +- src/Validators/Identical.php | 4 +- src/Validators/In.php | 4 +- src/Validators/Infinite.php | 4 +- src/Validators/Key.php | 6 +- src/Validators/KeyExists.php | 4 +- src/Validators/KeyOptional.php | 4 +- src/Validators/KeySet.php | 4 +- src/Validators/Length.php | 4 +- src/Validators/LessThan.php | 4 +- src/Validators/LessThanOrEqual.php | 4 +- src/Validators/Max.php | 4 +- src/Validators/Min.php | 4 +- src/Validators/Multiple.php | 4 +- src/Validators/Named.php | 4 +- src/Validators/Not.php | 4 +- src/Validators/NullOr.php | 7 +- src/Validators/Odd.php | 4 +- src/Validators/Positive.php | 4 +- src/Validators/Property.php | 6 +- src/Validators/PropertyExists.php | 4 +- src/Validators/PropertyOptional.php | 4 +- src/Validators/Templated.php | 4 +- src/Validators/Undef.php | 4 +- src/Validators/UndefOr.php | 7 +- .../src/Validators/NonPublic.php | 9 +- tests/unit/FluentValidatorFactoryTest.php | 127 +++++++++ 65 files changed, 615 insertions(+), 990 deletions(-) delete mode 100644 src-dev/CodeGen/Config.php delete mode 100644 src-dev/CodeGen/FluentBuilder/MethodBuilder.php delete mode 100644 src-dev/CodeGen/FluentBuilder/Mixin.php delete mode 100644 src-dev/CodeGen/FluentBuilder/MixinGenerator.php delete mode 100644 src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php delete mode 100644 src-dev/CodeGen/InterfaceConfig.php delete mode 100644 src-dev/CodeGen/NamespaceScanner.php delete mode 100644 src-dev/CodeGen/OutputFormatter.php create mode 100644 src/FluentValidatorFactory.php create mode 100644 src/Mixins/PrefixConstants.php delete mode 100644 src/Mixins/PrefixMap.php rename src-dev/CodeGen/CodeGenerator.php => tests/src/Validators/NonPublic.php (57%) create mode 100644 tests/unit/FluentValidatorFactoryTest.php diff --git a/composer.json b/composer.json index 0177f54d2..8940b57ba 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "php": ">=8.5", "php-di/php-di": "^7.1", "psr/container": "^2.0", + "respect/fluent": "^2.0", "respect/string-formatter": "^1.7", "respect/stringifier": "^3.0", "symfony/polyfill-intl-idn": "^1.33", @@ -43,6 +44,7 @@ "psr/http-message": "^1.0 || ^2.0", "ramsey/uuid": "^4", "respect/coding-standard": "^5.0", + "respect/fluentgen": "^2.0", "sebastian/diff": "^7.0", "sokil/php-isocodes": "^4.2.1", "sokil/php-isocodes-db-only": "^4.0", diff --git a/composer.lock b/composer.lock index 8a5920080..064ad11aa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9851274a86bdca1f0835e29e06bd8858", + "content-hash": "787a9854ff13231a29b86db328a562b3", "packages": [ { "name": "laravel/serializable-closure", - "version": "v2.0.9", + "version": "v2.0.10", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "8f631589ab07b7b52fead814965f5a800459cb3e" + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/8f631589ab07b7b52fead814965f5a800459cb3e", - "reference": "8f631589ab07b7b52fead814965f5a800459cb3e", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", "shasum": "" }, "require": { @@ -65,7 +65,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-02-03T06:55:34+00:00" + "time": "2026-02-20T19:59:49+00:00" }, { "name": "php-di/invoker", @@ -248,6 +248,59 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "respect/fluent", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/Respect/Fluent.git", + "reference": "21e9936c4ae753691e895bd4b68ab3380f482141" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/Fluent/zipball/21e9936c4ae753691e895bd4b68ab3380f482141", + "reference": "21e9936c4ae753691e895bd4b68ab3380f482141", + "shasum": "" + }, + "require": { + "php": "^8.5" + }, + "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": { + "Respect\\Fluent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Respect/Fluent Contributors", + "homepage": "https://github.com/Respect/Fluent/graphs/contributors" + } + ], + "description": "Namespace-aware fluent class resolution", + "keywords": [ + "builder", + "fluent", + "respect" + ], + "support": { + "issues": "https://github.com/Respect/Fluent/issues", + "source": "https://github.com/Respect/Fluent/tree/2.0.0" + }, + "time": "2026-03-25T05:08:46+00:00" + }, { "name": "respect/string-formatter", "version": "1.7.0", @@ -706,16 +759,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.19.0", + "version": "v7.19.2", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", "shasum": "" }, "require": { @@ -729,9 +782,9 @@ "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", "phpunit/php-file-iterator": "^6.0.1 || ^7", "phpunit/php-timer": "^8 || ^9", - "phpunit/phpunit": "^12.5.9 || ^13", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", "sebastian/environment": "^8.0.3 || ^9", - "symfony/console": "^7.4.4 || ^8.0.4", + "symfony/console": "^7.4.7 || ^8.0.7", "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { @@ -739,11 +792,11 @@ "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.38", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.12", - "phpstan/phpstan-strict-rules": "^2.0.8", - "symfony/filesystem": "^7.4.0 || ^8.0.1" + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" }, "bin": [ "bin/paratest", @@ -783,7 +836,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.19.2" }, "funding": [ { @@ -795,20 +848,20 @@ "type": "paypal" } ], - "time": "2026-02-06T10:53:26+00:00" + "time": "2026-03-09T14:33:17+00:00" }, { "name": "brick/math", - "version": "0.14.7", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/07ff363b16ef8aca9692bba3be9e73fe63f34e50", - "reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { @@ -847,7 +900,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.7" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -855,7 +908,7 @@ "type": "github" } ], - "time": "2026-02-07T10:57:35+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -2068,20 +2121,20 @@ }, { "name": "pestphp/pest", - "version": "v4.4.2", + "version": "v4.4.3", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701" + "reference": "e6ab897594312728ef2e32d586cb4f6780b1b495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", - "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", + "url": "https://api.github.com/repos/pestphp/pest/zipball/e6ab897594312728ef2e32d586cb4f6780b1b495", + "reference": "e6ab897594312728ef2e32d586cb4f6780b1b495", "shasum": "" }, "require": { - "brianium/paratest": "^7.19.0", + "brianium/paratest": "^7.19.2", "nunomaduro/collision": "^8.9.1", "nunomaduro/termwind": "^2.4.0", "pestphp/pest-plugin": "^4.0.0", @@ -2089,12 +2142,12 @@ "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.5.12", + "phpunit/phpunit": "^12.5.14", "symfony/process": "^7.4.5|^8.0.5" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.12", + "phpunit/phpunit": ">12.5.14", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, @@ -2168,7 +2221,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.4.2" + "source": "https://github.com/pestphp/pest/tree/v4.4.3" }, "funding": [ { @@ -2180,7 +2233,7 @@ "type": "github" } ], - "time": "2026-03-10T21:09:12+00:00" + "time": "2026-03-21T13:14:39+00:00" }, { "name": "pestphp/pest-plugin", @@ -2625,16 +2678,16 @@ }, { "name": "phpbench/phpbench", - "version": "1.5.1", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c" + "reference": "661c8c6abbc7734986cf7bc6062c237fbb450461" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c", - "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/661c8c6abbc7734986cf7bc6062c237fbb450461", + "reference": "661c8c6abbc7734986cf7bc6062c237fbb450461", "shasum": "" }, "require": { @@ -2712,7 +2765,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.5.1" + "source": "https://github.com/phpbench/phpbench/tree/1.6.1" }, "funding": [ { @@ -2720,7 +2773,7 @@ "type": "github" } ], - "time": "2026-03-05T08:18:58+00:00" + "time": "2026-03-22T10:27:20+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2777,16 +2830,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/897b5986ece6b4f9d8413fea345c7d49c757d6bf", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -2836,9 +2889,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-01T18:43:49+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2995,11 +3048,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.42", + "version": "2.1.43", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", - "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d01bebe3edfd4d49b9666ee5b8271ddca561042f", + "reference": "d01bebe3edfd4d49b9666ee5b8271ddca561042f", "shasum": "" }, "require": { @@ -3044,7 +3097,7 @@ "type": "github" } ], - "time": "2026-03-17T14:58:32+00:00" + "time": "2026-03-24T20:40:50+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -3500,16 +3553,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.12", + "version": "12.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199" + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/418e06b3b46b0d54bad749ff4907fc7dfb530199", - "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", "shasum": "" }, "require": { @@ -3578,7 +3631,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" }, "funding": [ { @@ -3602,7 +3655,7 @@ "type": "tidelift" } ], - "time": "2026-02-16T08:34:36+00:00" + "time": "2026-02-18T12:38:40+00:00" }, { "name": "psr/cache", @@ -4004,6 +4057,64 @@ }, "time": "2026-01-19T10:34:07+00:00" }, + { + "name": "respect/fluentgen", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/Respect/FluentGen.git", + "reference": "6a9065516f403c5f5abc86646290bd08e44c538e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/FluentGen/zipball/6a9065516f403c5f5abc86646290bd08e44c538e", + "reference": "6a9065516f403c5f5abc86646290bd08e44c538e", + "shasum": "" + }, + "require": { + "nette/php-generator": "^4.1", + "php": "^8.5" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^12.5", + "respect/coding-standard": "^5.0", + "respect/fluent": "^2.0" + }, + "suggest": { + "respect/fluent": "Enables #[Composable] prefix composition support" + }, + "type": "library", + "autoload": { + "psr-4": { + "Respect\\FluentGen\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Respect/FluentGen Contributors", + "homepage": "https://github.com/Respect/FluentGen/graphs/contributors" + } + ], + "description": "Code generation for fluent builder interfaces", + "keywords": [ + "fluent", + "fluentgen", + "mixin", + "respect" + ], + "support": { + "issues": "https://github.com/Respect/FluentGen/issues", + "source": "https://github.com/Respect/FluentGen/tree/2.0.0" + }, + "time": "2026-03-25T05:50:09+00:00" + }, { "name": "sebastian/cli-parser", "version": "4.2.0", @@ -4292,16 +4403,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.3", + "version": "8.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", "shasum": "" }, "require": { @@ -4344,7 +4455,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" }, "funding": [ { @@ -4364,7 +4475,7 @@ "type": "tidelift" } ], - "time": "2025-08-12T14:11:56+00:00" + "time": "2026-03-15T07:05:40+00:00" }, { "name": "sebastian/exporter", @@ -4967,32 +5078,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.27.1", + "version": "8.28.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "29bdaee8b65e7ed2b8e702b01852edba8bae1769" + "reference": "66151cfbd25b50e8becd9f809fb704f01fd4d6f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/29bdaee8b65e7ed2b8e702b01852edba8bae1769", - "reference": "29bdaee8b65e7ed2b8e702b01852edba8bae1769", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/66151cfbd25b50e8becd9f809fb704f01fd4d6f2", + "reference": "66151cfbd25b50e8becd9f809fb704f01fd4d6f2", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.2.0", "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.3.1", + "phpstan/phpdoc-parser": "^2.3.2", "squizlabs/php_codesniffer": "^4.0.1" }, "require-dev": { - "phing/phing": "3.0.1|3.1.1", + "phing/phing": "3.0.1|3.1.2", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.37", - "phpstan/phpstan-deprecation-rules": "2.0.3", - "phpstan/phpstan-phpunit": "2.0.12", - "phpstan/phpstan-strict-rules": "2.0.7", - "phpunit/phpunit": "9.6.31|10.5.60|11.4.4|11.5.49|12.5.7" + "phpstan/phpstan": "2.1.42", + "phpstan/phpstan-deprecation-rules": "2.0.4", + "phpstan/phpstan-phpunit": "2.0.16", + "phpstan/phpstan-strict-rules": "2.0.10", + "phpunit/phpunit": "9.6.34|10.5.63|11.4.4|11.5.50|12.5.14" }, "type": "phpcodesniffer-standard", "extra": { @@ -5016,7 +5127,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.27.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.28.1" }, "funding": [ { @@ -5028,7 +5139,7 @@ "type": "tidelift" } ], - "time": "2026-01-25T15:57:07+00:00" + "time": "2026-03-22T17:22:38+00:00" }, { "name": "sokil/php-isocodes", diff --git a/src-dev/CodeGen/Config.php b/src-dev/CodeGen/Config.php deleted file mode 100644 index af3a8864f..000000000 --- a/src-dev/CodeGen/Config.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -final readonly class Config -{ - public function __construct( - public string $sourceDir, - public string $sourceNamespace, - public string $outputDir, - public string $outputNamespace, - public OutputFormatter $outputFormatter = new OutputFormatter(), - ) { - } -} diff --git a/src-dev/CodeGen/FluentBuilder/MethodBuilder.php b/src-dev/CodeGen/FluentBuilder/MethodBuilder.php deleted file mode 100644 index 72da85787..000000000 --- a/src-dev/CodeGen/FluentBuilder/MethodBuilder.php +++ /dev/null @@ -1,169 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\Method; -use Nette\PhpGenerator\PhpNamespace; -use ReflectionClass; -use ReflectionNamedType; -use ReflectionParameter; -use ReflectionUnionType; - -use function count; -use function implode; -use function in_array; -use function is_object; -use function lcfirst; -use function preg_replace; -use function sort; -use function str_starts_with; -use function ucfirst; - -final class MethodBuilder -{ - /** - * @param array $excludedTypePrefixes - * @param array $excludedTypeNames - */ - public function __construct( - private readonly array $excludedTypePrefixes = [], - private readonly array $excludedTypeNames = [], - ) { - } - - public function build( - PhpNamespace $namespace, - ReflectionClass $nodeReflection, - string $returnType, - string|null $prefix = null, - bool $static = false, - ReflectionParameter|null $prefixParameter = null, - ): Method { - $originalName = $nodeReflection->getShortName(); - $name = $prefix ? $prefix . ucfirst($originalName) : lcfirst($originalName); - - $method = new Method($name); - $method->setPublic()->setReturnType($returnType); - - if ($static) { - $method->setStatic(); - } - - if ($prefixParameter !== null) { - $this->addPrefixParameter($method, $prefixParameter); - } - - $constructor = $nodeReflection->getConstructor(); - if ($constructor === null) { - return $method; - } - - $comment = $constructor->getDocComment(); - if ($comment) { - $method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $comment)); - } - - foreach ($constructor->getParameters() as $reflectionParameter) { - $this->addParameter($method, $reflectionParameter, $namespace); - } - - return $method; - } - - private function addPrefixParameter(Method $method, ReflectionParameter $reflectionParameter): void - { - $type = $reflectionParameter->getType(); - $types = []; - - if ($type instanceof ReflectionUnionType) { - foreach ($type->getTypes() as $subType) { - $types[] = $subType->getName(); - } - - sort($types); - } elseif ($type instanceof ReflectionNamedType) { - $types[] = $type->getName(); - } - - $method->addParameter($reflectionParameter->getName())->setType(implode('|', $types)); - } - - private function addParameter( - Method $method, - ReflectionParameter $reflectionParameter, - PhpNamespace $namespace, - ): void { - if ($reflectionParameter->isVariadic()) { - $method->setVariadic(); - } - - $type = $reflectionParameter->getType(); - $types = []; - - if ($type instanceof ReflectionUnionType) { - foreach ($type->getTypes() as $subType) { - $types[] = $subType->getName(); - if ($subType->isBuiltin()) { - continue; - } - - $namespace->addUse($subType->getName()); - } - } elseif ($type instanceof ReflectionNamedType) { - $types[] = $type->getName(); - - if ($this->isExcludedType($type->getName())) { - return; - } - - if (!$type->isBuiltin()) { - $namespace->addUse($type->getName()); - } - } - - $parameter = $method->addParameter($reflectionParameter->getName()); - $parameter->setType(implode('|', $types)); - - if (!$reflectionParameter->isDefaultValueAvailable()) { - $parameter->setNullable($reflectionParameter->isOptional()); - } - - if (count($types) > 1 || $reflectionParameter->isVariadic()) { - $parameter->setNullable(false); - } - - if (!$reflectionParameter->isDefaultValueAvailable()) { - return; - } - - $defaultValue = $reflectionParameter->getDefaultValue(); - if (is_object($defaultValue)) { - $parameter->setDefaultValue(null); - $parameter->setNullable(true); - - return; - } - - $parameter->setDefaultValue($defaultValue); - $parameter->setNullable(false); - } - - private function isExcludedType(string $typeName): bool - { - foreach ($this->excludedTypePrefixes as $excludedPrefix) { - if (str_starts_with($typeName, $excludedPrefix)) { - return true; - } - } - - return in_array($typeName, $this->excludedTypeNames, true); - } -} diff --git a/src-dev/CodeGen/FluentBuilder/Mixin.php b/src-dev/CodeGen/FluentBuilder/Mixin.php deleted file mode 100644 index 0e211711c..000000000 --- a/src-dev/CodeGen/FluentBuilder/Mixin.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Attribute; - -#[Attribute(Attribute::TARGET_CLASS)] -final readonly class Mixin -{ - /** - * @param array $exclude - * @param array $include - */ - public function __construct( - public string|null $prefix = null, - public bool $prefixParameter = false, - public bool $requireInclusion = false, - public array $exclude = [], - public array $include = [], - ) { - } -} diff --git a/src-dev/CodeGen/FluentBuilder/MixinGenerator.php b/src-dev/CodeGen/FluentBuilder/MixinGenerator.php deleted file mode 100644 index 3f605873a..000000000 --- a/src-dev/CodeGen/FluentBuilder/MixinGenerator.php +++ /dev/null @@ -1,253 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\PhpNamespace; -use Nette\PhpGenerator\Printer; -use ReflectionClass; -use ReflectionParameter; -use Respect\Dev\CodeGen\CodeGenerator; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\InterfaceConfig; -use Respect\Dev\CodeGen\NamespaceScanner; - -use function file_get_contents; -use function in_array; -use function is_file; -use function is_readable; -use function ksort; - -final class MixinGenerator implements CodeGenerator -{ - /** @param array $interfaces */ - public function __construct( - private readonly Config $config, - private readonly MethodBuilder $methodBuilder = new MethodBuilder(), - private readonly array $interfaces = [], - ) { - } - - /** @return array filename => content */ - public function generate(): array - { - $nodes = NamespaceScanner::scan($this->config->sourceDir, $this->config->sourceNamespace); - $prefixes = $this->discoverPrefixes($nodes); - $filters = $this->discoverFilters($nodes); - - $files = []; - - foreach ($this->interfaces as $interfaceConfig) { - $prefixInterfaceNames = []; - - foreach ($prefixes as $prefix) { - $interfaceName = $prefix['name'] . $interfaceConfig->suffix; - $prefixInterfaceNames[] = $this->config->outputNamespace . '\\' . $interfaceName; - - $this->generateInterface( - $interfaceName, - $interfaceConfig, - $nodes, - $filters, - $prefix, - $files, - ); - } - - $this->generateRootInterface( - $interfaceConfig, - $prefixInterfaceNames, - $nodes, - $filters, - $files, - ); - } - - return $files; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverPrefixes(array $nodes): array - { - $prefixes = []; - - foreach ($nodes as $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $mixin = $attributes[0]->newInstance(); - if ($mixin->prefix === null) { - continue; - } - - $constructor = $reflection->getConstructor(); - $prefixParameter = null; - - if ($mixin->prefixParameter && $constructor !== null) { - $parameters = $constructor->getParameters(); - if ($parameters !== []) { - $prefixParameter = $parameters[0]; - } - } - - $prefixes[$mixin->prefix] = [ - 'name' => $reflection->getShortName(), - 'prefix' => $mixin->prefix, - 'requireInclusion' => $mixin->requireInclusion, - 'prefixParameter' => $prefixParameter, - ]; - } - - ksort($prefixes); - - return $prefixes; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverFilters(array $nodes): array - { - $filters = []; - - foreach ($nodes as $name => $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $filters[$name] = $attributes[0]->newInstance(); - } - - return $filters; - } - - /** - * @param array $nodes - * @param array $filters - * @param array{name: string, prefix: string, requireInclusion: bool, prefixParameter: ?ReflectionParameter} $prefix - * @param array $files - */ - private function generateInterface( - string $interfaceName, - InterfaceConfig $config, - array $nodes, - array $filters, - array $prefix, - array &$files, - ): void { - $namespace = new PhpNamespace($this->config->outputNamespace); - $interface = $namespace->addInterface($interfaceName); - - foreach ($nodes as $name => $reflection) { - $mixin = $filters[$name] ?? null; - - if ($prefix['requireInclusion']) { - if ($mixin === null || !in_array($prefix['prefix'], $mixin->include, true)) { - continue; - } - } elseif ($mixin !== null && in_array($prefix['prefix'], $mixin->exclude, true)) { - continue; - } - - $method = $this->methodBuilder->build( - $namespace, - $reflection, - $config->returnType, - $prefix['prefix'], - $config->static, - $prefix['prefixParameter'], - ); - - $interface->addMember($method); - } - - $this->addFile($interfaceName, $namespace, $files); - } - - /** - * @param array $prefixInterfaceNames - * @param array $nodes - * @param array $filters - * @param array $files - */ - private function generateRootInterface( - InterfaceConfig $config, - array $prefixInterfaceNames, - array $nodes, - array $filters, - array &$files, - ): void { - $interfaceName = $config->suffix; - $namespace = new PhpNamespace($this->config->outputNamespace); - $interface = $namespace->addInterface($interfaceName); - - foreach ($config->rootExtends as $extend) { - $namespace->addUse($extend); - $interface->addExtend($extend); - } - - foreach ($prefixInterfaceNames as $prefixInterfaceName) { - $namespace->addUse($prefixInterfaceName); - $interface->addExtend($prefixInterfaceName); - } - - if ($config->rootComment !== null) { - $interface->addComment($config->rootComment); - } - - foreach ($config->rootUses as $use) { - $namespace->addUse($use); - } - - foreach ($nodes as $reflection) { - $method = $this->methodBuilder->build( - $namespace, - $reflection, - $config->returnType, - null, - $config->static, - ); - - $interface->addMember($method); - } - - $this->addFile($interfaceName, $namespace, $files); - } - - /** @param array $files */ - private function addFile(string $interfaceName, PhpNamespace $namespace, array &$files): void - { - $filename = $this->config->outputDir . '/' . $interfaceName . '.php'; - - $printer = new Printer(); - $printer->wrapLength = 300; - - $existingContent = ''; - if (is_file($filename) && is_readable($filename)) { - $existingContent = file_get_contents($filename) ?: ''; - } - - $formattedContent = $this->config->outputFormatter->format( - $printer->printNamespace($namespace), - $existingContent, - ); - - $files[$filename] = $formattedContent; - } -} diff --git a/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php b/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php deleted file mode 100644 index a91a9c63f..000000000 --- a/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php +++ /dev/null @@ -1,160 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\PhpNamespace; -use Nette\PhpGenerator\Printer; -use ReflectionClass; -use Respect\Dev\CodeGen\CodeGenerator; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\NamespaceScanner; - -use function array_keys; -use function ctype_upper; -use function file_get_contents; -use function is_file; -use function is_readable; -use function ksort; -use function lcfirst; -use function str_starts_with; -use function strlen; -use function uksort; - -final class PrefixMapGenerator implements CodeGenerator -{ - public function __construct( - private readonly Config $config, - private readonly string $outputClassName, - ) { - } - - /** @return array filename => content */ - public function generate(): array - { - $nodes = NamespaceScanner::scan($this->config->sourceDir, $this->config->sourceNamespace); - $prefixes = $this->discoverPrefixes($nodes); - $composable = $this->buildComposable($nodes, $prefixes); - $composableWithArgument = $this->buildComposableWithArgument($prefixes); - - $namespace = new PhpNamespace($this->config->outputNamespace); - $class = $namespace->addClass($this->outputClassName); - $class->setFinal(); - - $class->addConstant('COMPOSABLE', $composable)->setPublic()->setType('array'); - $class->addConstant('COMPOSABLE_WITH_ARGUMENT', $composableWithArgument)->setPublic()->setType('array'); - - $printer = new Printer(); - $printer->wrapLength = 300; - - $outputFile = $this->config->outputDir . '/' . $this->outputClassName . '.php'; - - $existingContent = ''; - if (is_file($outputFile) && is_readable($outputFile)) { - $existingContent = file_get_contents($outputFile) ?: ''; - } - - $formattedContent = $this->config->outputFormatter->format( - $printer->printNamespace($namespace), - $existingContent, - ); - - return [$outputFile => $formattedContent]; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverPrefixes(array $nodes): array - { - $prefixes = []; - - foreach ($nodes as $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $mixin = $attributes[0]->newInstance(); - if ($mixin->prefix === null) { - continue; - } - - $prefixes[$mixin->prefix] = [ - 'prefix' => $mixin->prefix, - 'prefixParameter' => $mixin->prefixParameter, - ]; - } - - ksort($prefixes); - - return $prefixes; - } - - /** - * @param array $nodes - * @param array $prefixes - * - * @return array - */ - private function buildComposable(array $nodes, array $prefixes): array - { - $composable = []; - - foreach (array_keys($prefixes) as $prefix) { - $composable[$prefix] = true; - - foreach (array_keys($nodes) as $name) { - $lcName = lcfirst($name); - if ($lcName === $prefix) { - continue; - } - - if (!str_starts_with($lcName, $prefix)) { - continue; - } - - if (!ctype_upper($lcName[strlen($prefix)])) { - continue; - } - - $composable[$lcName] = true; - } - } - - uksort($composable, static fn(string $a, string $b): int => strlen($b) <=> strlen($a) ?: $a <=> $b); - - return $composable; - } - - /** - * @param array $prefixes - * - * @return array - */ - private function buildComposableWithArgument(array $prefixes): array - { - $composableWithArgument = []; - - foreach ($prefixes as $prefix => $info) { - if (!$info['prefixParameter']) { - continue; - } - - $composableWithArgument[$prefix] = true; - } - - ksort($composableWithArgument); - - return $composableWithArgument; - } -} diff --git a/src-dev/CodeGen/InterfaceConfig.php b/src-dev/CodeGen/InterfaceConfig.php deleted file mode 100644 index 92b16482d..000000000 --- a/src-dev/CodeGen/InterfaceConfig.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -final readonly class InterfaceConfig -{ - /** - * @param array $rootExtends - * @param array $rootUses - */ - public function __construct( - public string $suffix, - public string $returnType, - public bool $static = false, - public array $rootExtends = [], - public string|null $rootComment = null, - public array $rootUses = [], - ) { - } -} diff --git a/src-dev/CodeGen/NamespaceScanner.php b/src-dev/CodeGen/NamespaceScanner.php deleted file mode 100644 index 5f9066aa4..000000000 --- a/src-dev/CodeGen/NamespaceScanner.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -use DirectoryIterator; -use ReflectionClass; - -use function ksort; - -final class NamespaceScanner -{ - /** @return array */ - public static function scan(string $directory, string $namespace): array - { - $nodes = []; - - foreach (new DirectoryIterator($directory) as $file) { - if (!$file->isFile()) { - continue; - } - - $className = $namespace . '\\' . $file->getBasename('.php'); - $reflection = new ReflectionClass($className); - - if ($reflection->isAbstract()) { - continue; - } - - $nodes[$reflection->getShortName()] = $reflection; - } - - ksort($nodes); - - return $nodes; - } -} diff --git a/src-dev/CodeGen/OutputFormatter.php b/src-dev/CodeGen/OutputFormatter.php deleted file mode 100644 index 9b9066ce8..000000000 --- a/src-dev/CodeGen/OutputFormatter.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -use function array_keys; -use function array_values; -use function implode; -use function preg_match; -use function preg_replace; -use function trim; - -use const PHP_EOL; - -final class OutputFormatter -{ - public function format(string $content, string $existingContent): string - { - preg_match('/^<\?php\s*\/\*[\s\S]*?\*\//', $existingContent, $matches); - $existingHeader = $matches[0] ?? ''; - - $replacements = [ - '/\n\n\t(public|private|\/\*\*)/m' => PHP_EOL . ' $1', - '/\t/m' => ' ', - '/\?([a-zA-Z]+) \$/' => '$1|null $', - '/\/\*\*\n +\* (.+)\n +\*\//m' => '/** $1 */', - ]; - - return implode(PHP_EOL, [ - trim($existingHeader) . PHP_EOL, - 'declare(strict_types=1);', - '', - preg_replace( - array_keys($replacements), - array_values($replacements), - $content, - ), - ]); - } -} diff --git a/src-dev/Commands/LintMixinCommand.php b/src-dev/Commands/LintMixinCommand.php index d9c6828de..c9fb1db2d 100644 --- a/src-dev/Commands/LintMixinCommand.php +++ b/src-dev/Commands/LintMixinCommand.php @@ -11,13 +11,14 @@ namespace Respect\Dev\Commands; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\FluentBuilder\MethodBuilder; -use Respect\Dev\CodeGen\FluentBuilder\MixinGenerator; -use Respect\Dev\CodeGen\FluentBuilder\PrefixMapGenerator; -use Respect\Dev\CodeGen\InterfaceConfig; use Respect\Dev\Differ\ConsoleDiffer; use Respect\Dev\Differ\Item; +use Respect\FluentGen\Config; +use Respect\FluentGen\Fluent\InterfaceConfig; +use Respect\FluentGen\Fluent\MethodBuilder; +use Respect\FluentGen\Fluent\MixinGenerator; +use Respect\FluentGen\Fluent\PrefixConstantsGenerator; +use Respect\FluentGen\NamespaceScanner; use Respect\Validation\Mixins\Chain; use Respect\Validation\Validator; use Respect\Validation\ValidatorBuilder; @@ -67,8 +68,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int outputNamespace: 'Respect\\Validation\\Mixins', ); + $scanner = new NamespaceScanner(); + $generator = new MixinGenerator( config: $config, + scanner: $scanner, methodBuilder: new MethodBuilder( excludedTypePrefixes: ['Sokil', 'Egulias'], excludedTypeNames: ['finfo'], @@ -89,9 +93,10 @@ interfaces: [ ], ); - $prefixMapGenerator = new PrefixMapGenerator( + $prefixMapGenerator = new PrefixConstantsGenerator( config: $config, - outputClassName: 'PrefixMap', + scanner: $scanner, + outputClassName: 'PrefixConstants', ); $files = $generator->generate() + $prefixMapGenerator->generate(); diff --git a/src/ContainerRegistry.php b/src/ContainerRegistry.php index 0dcd8c536..88200ce3d 100644 --- a/src/ContainerRegistry.php +++ b/src/ContainerRegistry.php @@ -14,6 +14,10 @@ use DI\Container; use libphonenumber\PhoneNumberUtil; use Psr\Container\ContainerInterface; +use Respect\Fluent\Factories\ComposingLookup; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\ComposableMap; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\StringFormatter\BypassTranslator; use Respect\StringFormatter\Modifier; use Respect\StringFormatter\Modifiers\FormatterModifier; @@ -40,13 +44,14 @@ use Respect\Validation\Message\Parameters\ResultHandler; use Respect\Validation\Message\Renderer; use Respect\Validation\Message\TemplateRegistry; -use Respect\Validation\Transformers\Prefix; -use Respect\Validation\Transformers\Transformer; +use Respect\Validation\Mixins\PrefixConstants; use Symfony\Contracts\Translation\TranslatorInterface; +use function array_map; use function DI\autowire; use function DI\create; use function DI\factory; +use function trim; final class ContainerRegistry { @@ -57,7 +62,6 @@ public static function createContainer(array $definitions = []): Container { return new Container($definitions + [ PhoneNumberUtil::class => factory(static fn() => PhoneNumberUtil::getInstance()), - Transformer::class => create(Prefix::class), TemplateRegistry::class => create(TemplateRegistry::class), TemplateResolver::class => autowire(TemplateResolver::class), TranslatorInterface::class => autowire(BypassTranslator::class), @@ -68,10 +72,26 @@ public static function createContainer(array $definitions = []): Container 'respect.validation.formatter.messages' => autowire(NestedArrayFormatter::class), 'respect.validation.ignored_backtrace_paths' => [__DIR__ . '/ValidatorBuilder.php'], 'respect.validation.rule_factory.namespaces' => ['Respect\\Validation\\Validators'], - ValidatorFactory::class => factory(static fn(Container $container) => new NamespacedValidatorFactory( - $container->get(Transformer::class), - $container->get('respect.validation.rule_factory.namespaces'), - )), + ValidatorFactory::class => factory(static function (Container $container) { + $namespaces = array_map( + static fn($ns) => trim($ns, '\\'), + $container->get('respect.validation.rule_factory.namespaces'), + ); + + return new FluentValidatorFactory( + new ComposingLookup( + new NamespaceLookup( + new Ucfirst(), + Validator::class, + ...$namespaces, + ), + new ComposableMap( + PrefixConstants::COMPOSABLE, + PrefixConstants::COMPOSABLE_WITH_ARGUMENT, + ), + ), + ); + }), Quoter::class => create(CodeQuoter::class)->constructor(120), Handler::class => factory(static function (Container $container) { $handler = CompositeHandler::create(); diff --git a/src/FluentValidatorFactory.php b/src/FluentValidatorFactory.php new file mode 100644 index 000000000..e1b0373f9 --- /dev/null +++ b/src/FluentValidatorFactory.php @@ -0,0 +1,53 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use Respect\Fluent\Exceptions\CouldNotCreate; +use Respect\Fluent\Exceptions\CouldNotResolve; +use Respect\Fluent\FluentFactory; +use Respect\Validation\Exceptions\ComponentException; +use Respect\Validation\Exceptions\InvalidClassException; + +use function sprintf; +use function trim; + +final readonly class FluentValidatorFactory implements ValidatorFactory +{ + public function __construct( + private FluentFactory $factory, + ) { + } + + /** @param array $arguments */ + public function create(string $ruleName, array $arguments = []): Validator + { + try { + $instance = $this->factory->create($ruleName, $arguments); + } catch (CouldNotResolve $e) { + throw new ComponentException(sprintf('"%s" is not a valid rule name', $ruleName), 0, $e); + } catch (CouldNotCreate $e) { + throw new InvalidClassException($e->getMessage(), 0, $e); + } + + if (!$instance instanceof Validator) { + throw new InvalidClassException( + sprintf('"%s" must be an instance of "%s"', $ruleName, Validator::class), + ); + } + + return $instance; + } + + public function withNamespace(string $rulesNamespace): self + { + return new self($this->factory->withNamespace(trim($rulesNamespace, '\\'))); + } +} diff --git a/src/Mixins/Builder.php b/src/Mixins/Builder.php index d4042eda0..431dabd04 100644 --- a/src/Mixins/Builder.php +++ b/src/Mixins/Builder.php @@ -182,11 +182,11 @@ public static function iterableVal(): Chain; public static function json(): Chain; - public static function key(string|int $key, Validator $validator): Chain; + public static function key(int|string $key, Validator $validator): Chain; - public static function keyExists(string|int $key): Chain; + public static function keyExists(int|string $key): Chain; - public static function keyOptional(string|int $key, Validator $validator): Chain; + public static function keyOptional(int|string $key, Validator $validator): Chain; public static function keySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/Chain.php b/src/Mixins/Chain.php index acc75aef5..850938790 100644 --- a/src/Mixins/Chain.php +++ b/src/Mixins/Chain.php @@ -184,11 +184,11 @@ public function iterableVal(): Chain; public function json(): Chain; - public function key(string|int $key, Validator $validator): Chain; + public function key(int|string $key, Validator $validator): Chain; - public function keyExists(string|int $key): Chain; + public function keyExists(int|string $key): Chain; - public function keyOptional(string|int $key, Validator $validator): Chain; + public function keyOptional(int|string $key, Validator $validator): Chain; public function keySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NotBuilder.php b/src/Mixins/NotBuilder.php index 41c6c44ab..225a5e009 100644 --- a/src/Mixins/NotBuilder.php +++ b/src/Mixins/NotBuilder.php @@ -179,11 +179,11 @@ public static function notIterableVal(): Chain; public static function notJson(): Chain; - public static function notKey(string|int $key, Validator $validator): Chain; + public static function notKey(int|string $key, Validator $validator): Chain; - public static function notKeyExists(string|int $key): Chain; + public static function notKeyExists(int|string $key): Chain; - public static function notKeyOptional(string|int $key, Validator $validator): Chain; + public static function notKeyOptional(int|string $key, Validator $validator): Chain; public static function notKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NotChain.php b/src/Mixins/NotChain.php index 113737fac..8ea5fd991 100644 --- a/src/Mixins/NotChain.php +++ b/src/Mixins/NotChain.php @@ -179,11 +179,11 @@ public function notIterableVal(): Chain; public function notJson(): Chain; - public function notKey(string|int $key, Validator $validator): Chain; + public function notKey(int|string $key, Validator $validator): Chain; - public function notKeyExists(string|int $key): Chain; + public function notKeyExists(int|string $key): Chain; - public function notKeyOptional(string|int $key, Validator $validator): Chain; + public function notKeyOptional(int|string $key, Validator $validator): Chain; public function notKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NullOrBuilder.php b/src/Mixins/NullOrBuilder.php index 12f54b0b2..e32cd0939 100644 --- a/src/Mixins/NullOrBuilder.php +++ b/src/Mixins/NullOrBuilder.php @@ -179,11 +179,11 @@ public static function nullOrIterableVal(): Chain; public static function nullOrJson(): Chain; - public static function nullOrKey(string|int $key, Validator $validator): Chain; + public static function nullOrKey(int|string $key, Validator $validator): Chain; - public static function nullOrKeyExists(string|int $key): Chain; + public static function nullOrKeyExists(int|string $key): Chain; - public static function nullOrKeyOptional(string|int $key, Validator $validator): Chain; + public static function nullOrKeyOptional(int|string $key, Validator $validator): Chain; public static function nullOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NullOrChain.php b/src/Mixins/NullOrChain.php index 329659911..66e87b606 100644 --- a/src/Mixins/NullOrChain.php +++ b/src/Mixins/NullOrChain.php @@ -179,11 +179,11 @@ public function nullOrIterableVal(): Chain; public function nullOrJson(): Chain; - public function nullOrKey(string|int $key, Validator $validator): Chain; + public function nullOrKey(int|string $key, Validator $validator): Chain; - public function nullOrKeyExists(string|int $key): Chain; + public function nullOrKeyExists(int|string $key): Chain; - public function nullOrKeyOptional(string|int $key, Validator $validator): Chain; + public function nullOrKeyOptional(int|string $key, Validator $validator): Chain; public function nullOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/PrefixConstants.php b/src/Mixins/PrefixConstants.php new file mode 100644 index 000000000..9afcf99e2 --- /dev/null +++ b/src/Mixins/PrefixConstants.php @@ -0,0 +1,56 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Mixins; + +final class PrefixConstants +{ + public const array COMPOSABLE = [ + 'propertyOptional' => true, + 'propertyExists' => true, + 'keyOptional' => true, + 'keyExists' => true, + 'property' => true, + 'undefOr' => true, + 'keySet' => true, + 'length' => true, + 'nullOr' => true, + 'allOf' => true, + 'all' => true, + 'key' => true, + 'max' => true, + 'min' => true, + 'not' => true, + ]; + public const array COMPOSABLE_WITH_ARGUMENT = ['key' => true, 'property' => true]; + public const array FORBIDDEN = [ + 'All' => ['all' => true], + 'Attributes' => ['all' => true, 'key' => true, 'not' => true, 'property' => true, 'undefOr' => true], + 'Blank' => ['nullOr' => true, 'undefOr' => true], + 'Exists' => ['all' => true, 'key' => true, 'property' => true], + 'Formatted' => ['all' => true, 'key' => true, 'property' => true], + 'Key' => ['all' => true, 'key' => true, 'property' => true], + 'KeyExists' => ['all' => true, 'key' => true, 'property' => true], + 'KeyOptional' => ['all' => true, 'key' => true, 'property' => true], + 'KeySet' => ['all' => true, 'key' => true, 'property' => true], + 'Length' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Max' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Min' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Named' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Not' => ['not' => true], + 'NullOr' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Property' => ['all' => true, 'key' => true, 'property' => true], + 'PropertyExists' => ['all' => true, 'key' => true, 'property' => true], + 'PropertyOptional' => ['all' => true, 'key' => true, 'property' => true], + 'Templated' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Undef' => ['nullOr' => true, 'undefOr' => true], + 'UndefOr' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + ]; +} diff --git a/src/Mixins/PrefixMap.php b/src/Mixins/PrefixMap.php deleted file mode 100644 index d27356be4..000000000 --- a/src/Mixins/PrefixMap.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Validation\Mixins; - -final class PrefixMap -{ - public const array COMPOSABLE = [ - 'propertyOptional' => true, - 'propertyExists' => true, - 'keyOptional' => true, - 'keyExists' => true, - 'property' => true, - 'undefOr' => true, - 'keySet' => true, - 'length' => true, - 'nullOr' => true, - 'allOf' => true, - 'all' => true, - 'key' => true, - 'max' => true, - 'min' => true, - 'not' => true, - ]; - public const array COMPOSABLE_WITH_ARGUMENT = ['key' => true, 'property' => true]; -} diff --git a/src/Mixins/UndefOrBuilder.php b/src/Mixins/UndefOrBuilder.php index c8c6273ec..c03840e6b 100644 --- a/src/Mixins/UndefOrBuilder.php +++ b/src/Mixins/UndefOrBuilder.php @@ -177,11 +177,11 @@ public static function undefOrIterableVal(): Chain; public static function undefOrJson(): Chain; - public static function undefOrKey(string|int $key, Validator $validator): Chain; + public static function undefOrKey(int|string $key, Validator $validator): Chain; - public static function undefOrKeyExists(string|int $key): Chain; + public static function undefOrKeyExists(int|string $key): Chain; - public static function undefOrKeyOptional(string|int $key, Validator $validator): Chain; + public static function undefOrKeyOptional(int|string $key, Validator $validator): Chain; public static function undefOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/UndefOrChain.php b/src/Mixins/UndefOrChain.php index aee52fcae..7d04d2463 100644 --- a/src/Mixins/UndefOrChain.php +++ b/src/Mixins/UndefOrChain.php @@ -177,11 +177,11 @@ public function undefOrIterableVal(): Chain; public function undefOrJson(): Chain; - public function undefOrKey(string|int $key, Validator $validator): Chain; + public function undefOrKey(int|string $key, Validator $validator): Chain; - public function undefOrKeyExists(string|int $key): Chain; + public function undefOrKeyExists(int|string $key): Chain; - public function undefOrKeyOptional(string|int $key, Validator $validator): Chain; + public function undefOrKeyOptional(int|string $key, Validator $validator): Chain; public function undefOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Transformers/Prefix.php b/src/Transformers/Prefix.php index d6205bb84..6c33600cf 100644 --- a/src/Transformers/Prefix.php +++ b/src/Transformers/Prefix.php @@ -11,7 +11,7 @@ namespace Respect\Validation\Transformers; -use Respect\Validation\Mixins\PrefixMap; +use Respect\Validation\Mixins\PrefixConstants; use function array_keys; use function array_slice; @@ -30,7 +30,7 @@ public function transform(ValidatorSpec $validatorSpec): ValidatorSpec return $validatorSpec; } - if (!isset(PrefixMap::COMPOSABLE_WITH_ARGUMENT[$matches['prefix']])) { + if (!isset(PrefixConstants::COMPOSABLE_WITH_ARGUMENT[$matches['prefix']])) { return new ValidatorSpec( $matches['suffix'], $validatorSpec->arguments, @@ -48,7 +48,7 @@ public function transform(ValidatorSpec $validatorSpec): ValidatorSpec /** @return array{}|array{prefix: string, suffix: string} */ private function match(ValidatorSpec $validatorSpec): array { - if ($validatorSpec->wrapper !== null || isset(PrefixMap::COMPOSABLE[$validatorSpec->name])) { + if ($validatorSpec->wrapper !== null || isset(PrefixConstants::COMPOSABLE[$validatorSpec->name])) { return []; } @@ -65,7 +65,7 @@ private static function getRegex(): string { return self::$regex ?? self::$regex = sprintf( '/^(?%s)(?.+)$/', - implode('|', array_keys(PrefixMap::COMPOSABLE)), + implode('|', array_keys(PrefixConstants::COMPOSABLE)), ); } } diff --git a/src/ValidatorBuilder.php b/src/ValidatorBuilder.php index 5c279989f..d4af62934 100644 --- a/src/ValidatorBuilder.php +++ b/src/ValidatorBuilder.php @@ -12,12 +12,20 @@ namespace Respect\Validation; +use Respect\Fluent\Attributes\AssuranceAssertion; +use Respect\Fluent\Attributes\AssuranceParameter; +use Respect\Fluent\Attributes\FluentNamespace; +use Respect\Fluent\Factories\ComposingLookup; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\ComposableMap; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\Validation\Exceptions\ComponentException; use Respect\Validation\Exceptions\ValidationException; use Respect\Validation\Message\ArrayFormatter; use Respect\Validation\Message\Renderer; use Respect\Validation\Message\StringFormatter; use Respect\Validation\Mixins\Builder; +use Respect\Validation\Mixins\PrefixConstants; use Respect\Validation\Validators\AllOf; use Respect\Validation\Validators\Core\Nameable; use Respect\Validation\Validators\Core\ShortCircuitable; @@ -31,6 +39,14 @@ use function is_string; /** @mixin Builder */ +#[FluentNamespace(new ComposingLookup( + new NamespaceLookup(new Ucfirst(), Validator::class, 'Respect\\Validation\\Validators'), + new ComposableMap( + PrefixConstants::COMPOSABLE, + PrefixConstants::COMPOSABLE_WITH_ARGUMENT, + PrefixConstants::FORBIDDEN, + ), +))] final readonly class ValidatorBuilder implements Nameable, ShortCircuitable { /** @var array */ @@ -81,20 +97,31 @@ public function validate(mixed $input, array|string|null $template = null): Resu return $this->toResultQuery($this->evaluate($input), $template); } - public function isValid(mixed $input): bool - { + #[AssuranceAssertion] + public function isValid( + #[AssuranceParameter] + mixed $input, + ): bool { return $this->evaluateShortCircuit($input)->hasPassed; } /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ - public function check(mixed $input, array|string|Throwable|callable|null $template = null): void - { + #[AssuranceAssertion] + public function check( + #[AssuranceParameter] + mixed $input, + array|string|Throwable|callable|null $template = null, + ): void { $this->throwOnFailure($this->evaluateShortCircuit($input), $template); } /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ - public function assert(mixed $input, array|string|Throwable|callable|null $template = null): void - { + #[AssuranceAssertion] + public function assert( + #[AssuranceParameter] + mixed $input, + array|string|Throwable|callable|null $template = null, + ): void { $this->throwOnFailure($this->evaluate($input), $template); } diff --git a/src/Validators/All.php b/src/Validators/All.php index 6a7d728b5..4080e74cb 100644 --- a/src/Validators/All.php +++ b/src/Validators/All.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Path; @@ -23,7 +23,7 @@ use Respect\Validation\Validators\Core\FilteredArray; use Respect\Validation\Validators\Core\ShortCircuitable; -#[Mixin(prefix: 'all', exclude: ['all'])] +#[Composable(prefix: self::class, without: [self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('Every item in', 'Every item in')] final class All extends FilteredArray implements ShortCircuitable diff --git a/src/Validators/Attributes.php b/src/Validators/Attributes.php index 78ff90583..13210b052 100644 --- a/src/Validators/Attributes.php +++ b/src/Validators/Attributes.php @@ -17,13 +17,13 @@ use ReflectionClass; use ReflectionObject; use ReflectionProperty; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Id; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\Reducer; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class Attributes implements Validator { diff --git a/src/Validators/Between.php b/src/Validators/Between.php index 7ef768e25..862dd6a86 100644 --- a/src/Validators/Between.php +++ b/src/Validators/Between.php @@ -15,13 +15,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanCompareValues; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be between {{minValue}} and {{maxValue}}', diff --git a/src/Validators/BetweenExclusive.php b/src/Validators/BetweenExclusive.php index 41ee52218..3fd65054c 100644 --- a/src/Validators/BetweenExclusive.php +++ b/src/Validators/BetweenExclusive.php @@ -12,13 +12,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanCompareValues; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than {{minValue}} and less than {{maxValue}}', diff --git a/src/Validators/Blank.php b/src/Validators/Blank.php index 067864ce6..c83737ba6 100644 --- a/src/Validators/Blank.php +++ b/src/Validators/Blank.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -27,7 +27,7 @@ use function is_string; use function trim; -#[Mixin(exclude: ['nullOr', 'undefOr'])] +#[Composable(without: [NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be blank', diff --git a/src/Validators/Equals.php b/src/Validators/Equals.php index d998f5df0..04ab4932a 100644 --- a/src/Validators/Equals.php +++ b/src/Validators/Equals.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use function is_scalar; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be equal to {{compareTo}}', diff --git a/src/Validators/Equivalent.php b/src/Validators/Equivalent.php index fc3cf6247..a9c41a8b7 100644 --- a/src/Validators/Equivalent.php +++ b/src/Validators/Equivalent.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; use function is_scalar; use function mb_strtoupper; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be equivalent to {{compareTo}}', diff --git a/src/Validators/Even.php b/src/Validators/Even.php index 5208abf90..9ed4c8c4f 100644 --- a/src/Validators/Even.php +++ b/src/Validators/Even.php @@ -18,7 +18,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,7 +26,7 @@ use const FILTER_VALIDATE_INT; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an even number', diff --git a/src/Validators/Exists.php b/src/Validators/Exists.php index dee5bab98..c9b005ef4 100644 --- a/src/Validators/Exists.php +++ b/src/Validators/Exists.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -23,7 +23,7 @@ use function file_exists; use function is_string; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an existing file', diff --git a/src/Validators/Factor.php b/src/Validators/Factor.php index bfdcdcacc..9428d6c3e 100644 --- a/src/Validators/Factor.php +++ b/src/Validators/Factor.php @@ -14,7 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -24,7 +24,7 @@ use function is_numeric; use function preg_match; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a factor of {{dividend|raw}}', diff --git a/src/Validators/Finite.php b/src/Validators/Finite.php index 9c917861b..1b79d3944 100644 --- a/src/Validators/Finite.php +++ b/src/Validators/Finite.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_finite; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a finite number', diff --git a/src/Validators/Formatted.php b/src/Validators/Formatted.php index 23696da5c..eda66f220 100644 --- a/src/Validators/Formatted.php +++ b/src/Validators/Formatted.php @@ -11,12 +11,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\StringFormatter\Formatter; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Formatted implements Validator { diff --git a/src/Validators/GreaterThan.php b/src/Validators/GreaterThan.php index cafa657b5..af6f5cf9a 100644 --- a/src/Validators/GreaterThan.php +++ b/src/Validators/GreaterThan.php @@ -15,11 +15,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than {{compareTo}}', diff --git a/src/Validators/GreaterThanOrEqual.php b/src/Validators/GreaterThanOrEqual.php index 0d273fc23..41a22a2ac 100644 --- a/src/Validators/GreaterThanOrEqual.php +++ b/src/Validators/GreaterThanOrEqual.php @@ -14,11 +14,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than or equal to {{compareTo}}', diff --git a/src/Validators/Identical.php b/src/Validators/Identical.php index 25380531e..2fed245c8 100644 --- a/src/Validators/Identical.php +++ b/src/Validators/Identical.php @@ -15,12 +15,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be identical to {{compareTo}}', diff --git a/src/Validators/In.php b/src/Validators/In.php index 53d46d9b7..845626195 100644 --- a/src/Validators/In.php +++ b/src/Validators/In.php @@ -16,7 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -25,7 +25,7 @@ use function is_array; use function mb_strpos; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be in {{haystack}}', diff --git a/src/Validators/Infinite.php b/src/Validators/Infinite.php index 34bcc67cd..0d7ff13a3 100644 --- a/src/Validators/Infinite.php +++ b/src/Validators/Infinite.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_infinite; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an infinite number', diff --git a/src/Validators/Key.php b/src/Validators/Key.php index f7ac017ce..0308d1b5d 100644 --- a/src/Validators/Key.php +++ b/src/Validators/Key.php @@ -16,17 +16,19 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; +use Respect\Fluent\Attributes\ComposableParameter; use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\KeyRelated; -#[Mixin(prefix: 'key', prefixParameter: true, exclude: ['all', 'key', 'property'])] +#[Composable(prefix: self::class, without: [All::class, self::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Key implements KeyRelated { public function __construct( + #[ComposableParameter] private int|string $key, private Validator $validator, ) { diff --git a/src/Validators/KeyExists.php b/src/Validators/KeyExists.php index c8e0a73aa..f7bf8462b 100644 --- a/src/Validators/KeyExists.php +++ b/src/Validators/KeyExists.php @@ -13,7 +13,7 @@ use ArrayAccess; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; @@ -23,7 +23,7 @@ use function array_key_exists; use function is_array; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be present', diff --git a/src/Validators/KeyOptional.php b/src/Validators/KeyOptional.php index 3bd94596b..d802151e2 100644 --- a/src/Validators/KeyOptional.php +++ b/src/Validators/KeyOptional.php @@ -13,12 +13,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\KeyRelated; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class KeyOptional implements KeyRelated { diff --git a/src/Validators/KeySet.php b/src/Validators/KeySet.php index 499a66238..5f878d044 100644 --- a/src/Validators/KeySet.php +++ b/src/Validators/KeySet.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; @@ -33,7 +33,7 @@ use function array_merge; use function array_slice; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} validation failed', diff --git a/src/Validators/Length.php b/src/Validators/Length.php index feafa0ee6..5f7eead39 100644 --- a/src/Validators/Length.php +++ b/src/Validators/Length.php @@ -20,7 +20,7 @@ use Attribute; use Countable as PhpCountable; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -30,7 +30,7 @@ use function is_string; use function mb_strlen; -#[Mixin(prefix: 'length', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'The length of', diff --git a/src/Validators/LessThan.php b/src/Validators/LessThan.php index eb2039b69..715d98337 100644 --- a/src/Validators/LessThan.php +++ b/src/Validators/LessThan.php @@ -15,11 +15,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be less than {{compareTo}}', diff --git a/src/Validators/LessThanOrEqual.php b/src/Validators/LessThanOrEqual.php index 7734befb5..28e22730a 100644 --- a/src/Validators/LessThanOrEqual.php +++ b/src/Validators/LessThanOrEqual.php @@ -14,11 +14,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be less than or equal to {{compareTo}}', diff --git a/src/Validators/Max.php b/src/Validators/Max.php index 9d89607bb..d31f30374 100644 --- a/src/Validators/Max.php +++ b/src/Validators/Max.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use function max; -#[Mixin(prefix: 'max', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('The maximum of', 'The maximum of')] final class Max extends FilteredArray diff --git a/src/Validators/Min.php b/src/Validators/Min.php index 9083b43c8..6d1812cbf 100644 --- a/src/Validators/Min.php +++ b/src/Validators/Min.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use function min; -#[Mixin(prefix: 'min', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('The minimum of', 'The minimum of')] final class Min extends FilteredArray diff --git a/src/Validators/Multiple.php b/src/Validators/Multiple.php index e780ff802..891509ed8 100644 --- a/src/Validators/Multiple.php +++ b/src/Validators/Multiple.php @@ -17,12 +17,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a multiple of {{multipleOf}}', diff --git a/src/Validators/Named.php b/src/Validators/Named.php index db58b9dc4..7da7b0a5c 100644 --- a/src/Validators/Named.php +++ b/src/Validators/Named.php @@ -12,7 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Name; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -20,7 +20,7 @@ use function is_string; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final readonly class Named implements Nameable { diff --git a/src/Validators/Not.php b/src/Validators/Not.php index 1026bb6c6..224342acf 100644 --- a/src/Validators/Not.php +++ b/src/Validators/Not.php @@ -18,11 +18,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(prefix: 'not', exclude: ['not'])] +#[Composable(prefix: self::class, without: [self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final readonly class Not implements Validator { diff --git a/src/Validators/NullOr.php b/src/Validators/NullOr.php index 9b02f1500..072712b68 100644 --- a/src/Validators/NullOr.php +++ b/src/Validators/NullOr.php @@ -14,14 +14,17 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use function array_map; -#[Mixin(prefix: 'nullOr', exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable( + prefix: self::class, + without: [All::class, Key::class, Property::class, Not::class, self::class, UndefOr::class], +)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'or must be null', diff --git a/src/Validators/Odd.php b/src/Validators/Odd.php index bc0331fd7..231b929aa 100644 --- a/src/Validators/Odd.php +++ b/src/Validators/Odd.php @@ -17,7 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,7 +26,7 @@ use const FILTER_VALIDATE_INT; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an odd number', diff --git a/src/Validators/Positive.php b/src/Validators/Positive.php index 748180d63..88b010332 100644 --- a/src/Validators/Positive.php +++ b/src/Validators/Positive.php @@ -15,13 +15,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a positive number', diff --git a/src/Validators/Property.php b/src/Validators/Property.php index 023ab0e71..273f11706 100644 --- a/src/Validators/Property.php +++ b/src/Validators/Property.php @@ -19,16 +19,18 @@ use Attribute; use ReflectionClass; use ReflectionObject; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; +use Respect\Fluent\Attributes\ComposableParameter; use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(prefix: 'property', prefixParameter: true, exclude: ['all', 'key', 'property'])] +#[Composable(prefix: self::class, without: [All::class, Key::class, self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Property implements Validator { public function __construct( + #[ComposableParameter] private string $propertyName, private Validator $validator, ) { diff --git a/src/Validators/PropertyExists.php b/src/Validators/PropertyExists.php index efa55c00e..d207ece8d 100644 --- a/src/Validators/PropertyExists.php +++ b/src/Validators/PropertyExists.php @@ -14,7 +14,7 @@ use Attribute; use ReflectionClass; use ReflectionObject; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; @@ -22,7 +22,7 @@ use function is_object; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be present', diff --git a/src/Validators/PropertyOptional.php b/src/Validators/PropertyOptional.php index 56ed4f54e..519e948ef 100644 --- a/src/Validators/PropertyOptional.php +++ b/src/Validators/PropertyOptional.php @@ -13,11 +13,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class PropertyOptional implements Validator { diff --git a/src/Validators/Templated.php b/src/Validators/Templated.php index 4e02ff847..45fdc4ddd 100644 --- a/src/Validators/Templated.php +++ b/src/Validators/Templated.php @@ -12,11 +12,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final readonly class Templated implements Validator { diff --git a/src/Validators/Undef.php b/src/Validators/Undef.php index 2ea85a48a..19905afaa 100644 --- a/src/Validators/Undef.php +++ b/src/Validators/Undef.php @@ -16,13 +16,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['nullOr', 'undefOr'])] +#[Composable(without: [NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be undefined', diff --git a/src/Validators/UndefOr.php b/src/Validators/UndefOr.php index 576fd2def..ce8610eef 100644 --- a/src/Validators/UndefOr.php +++ b/src/Validators/UndefOr.php @@ -13,7 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -21,7 +21,10 @@ use function array_map; -#[Mixin(prefix: 'undefOr', exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable( + prefix: self::class, + without: [All::class, Key::class, Property::class, Not::class, NullOr::class, self::class], +)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'or must be undefined', diff --git a/src-dev/CodeGen/CodeGenerator.php b/tests/src/Validators/NonPublic.php similarity index 57% rename from src-dev/CodeGen/CodeGenerator.php rename to tests/src/Validators/NonPublic.php index 69007f32d..e2ea2fc25 100644 --- a/src-dev/CodeGen/CodeGenerator.php +++ b/tests/src/Validators/NonPublic.php @@ -8,10 +8,11 @@ declare(strict_types=1); -namespace Respect\Dev\CodeGen; +namespace Respect\Validation\Test\Validators; -interface CodeGenerator +final class NonPublic { - /** @return array filename => content */ - public function generate(): array; + private function __construct() + { + } } diff --git a/tests/unit/FluentValidatorFactoryTest.php b/tests/unit/FluentValidatorFactoryTest.php new file mode 100644 index 000000000..2aa0707a3 --- /dev/null +++ b/tests/unit/FluentValidatorFactoryTest.php @@ -0,0 +1,127 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use Error; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; +use Respect\Validation\Exceptions\ComponentException; +use Respect\Validation\Exceptions\InvalidClassException; +use Respect\Validation\Test\TestCase; +use Respect\Validation\Test\Validators\Stub; +use Respect\Validation\Test\Validators\Valid; + +#[Group('core')] +#[CoversClass(FluentValidatorFactory::class)] +final class FluentValidatorFactoryTest extends TestCase +{ + private const string TEST_NAMESPACE = 'Respect\\Validation\\Test\\Validators'; + + #[Test] + public function itShouldCreateValidatorByName(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_NAMESPACE), + ); + + self::assertInstanceOf(Valid::class, $factory->create('valid')); + } + + #[Test] + public function itShouldPassArgumentsToConstructor(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_NAMESPACE), + ); + + $validator = $factory->create('stub', [true, false, true]); + + self::assertInstanceOf(Stub::class, $validator); + self::assertSame([true, false, true], $validator->validations); + } + + #[Test] + public function itShouldThrowComponentExceptionWhenRuleIsNotFound(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_NAMESPACE), + ); + + $this->expectException(ComponentException::class); + $this->expectExceptionMessage('"nonExistingRule" is not a valid rule name'); + + $factory->create('nonExistingRule'); + } + + #[Test] + public function itShouldThrowInvalidClassExceptionWhenNonValidatorIsResolved(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), null, self::TEST_NAMESPACE), + ); + + $this->expectException(InvalidClassException::class); + $this->expectExceptionMessage('must be an instance of'); + + $factory->create('invalid'); + } + + #[Test] + public function itShouldBubbleUpErrorWhenInstantiationFails(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), null, self::TEST_NAMESPACE), + ); + + $this->expectException(Error::class); + + $factory->create('myAbstractClass'); + } + + #[Test] + public function itShouldThrowInvalidClassExceptionWhenInstantiationFails(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), null, self::TEST_NAMESPACE), + ); + + $this->expectException(InvalidClassException::class); + + $factory->create('nonPublic'); + } + + #[Test] + public function itShouldPrependNamespaceViaWithNamespace(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, 'NonExistent\\Namespace'), + ); + + $extended = $factory->withNamespace(self::TEST_NAMESPACE); + + self::assertInstanceOf(Valid::class, $extended->create('valid')); + } + + #[Test] + public function itShouldReturnNewInstanceFromWithNamespace(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_NAMESPACE), + ); + + $extended = $factory->withNamespace('Another\\Namespace'); + + self::assertNotSame($factory, $extended); + } +}