From fbc2247f57a71496544f9a6ad1dc3dacb95ceb80 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Wed, 25 Mar 2026 03:00:51 -0300 Subject: [PATCH] Integrate Fluent, FluentGen and FluentAnalysis Replace the internal code generation and builder infrastructure with Respect/Fluent for runtime method resolution, Respect/FluentGen for mixin interface generation, and Respect/FluentAnalysis for PHPStan type narrowing. ValidatorBuilder now extends Fluent's Append builder with ComposableMap for prefix composition at runtime. Each validator is annotated with: - #[Assurance] declaring the type it narrows to (100+ validators) - #[AssuranceParameter] on constructor params used for dynamic type resolution (Instance) - #[Composable] with class-string references for prefix composition constraints (Not, All, Key, Property, NullOr, UndefOr, Min, Max, Length, and 29 validators with with/without constraints) - #[ComposableParameter] on promoted prefix parameters (Key, Property) Removes the internal CodeGen infrastructure (MethodBuilder, MixinGenerator, PrefixMapGenerator, etc.) in favor of FluentGen. Adds type inference tests validating PHPStan narrowing for type validators, val variants, composites (allOf, anyOf, oneOf, noneOf, when), modifiers (not, nullOr, undefOr), element narrowing (each, all), value/member narrowing (identical, in), parameter narrowing (instance), and chain intersection. --- .github/workflows/ci-code.yml | 1 + REUSE.toml | 2 +- composer.json | 15 +- composer.lock | 358 +++++++++---- fluent.neon | 4 + phpcs.xml.dist | 4 + phpstan.neon.dist | 3 + phpunit.xml.dist | 3 + 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 | 26 +- 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/NamespacedValidatorFactory.php | 98 ---- src/Transformers/Prefix.php | 71 --- src/Transformers/Transformer.php | 17 - src/Transformers/ValidatorSpec.php | 23 - src/ValidatorBuilder.php | 81 +-- src/ValidatorFactory.php | 18 - src/Validators/All.php | 7 +- src/Validators/AllOf.php | 3 + src/Validators/Alnum.php | 2 + src/Validators/Alpha.php | 2 + src/Validators/AnyOf.php | 3 + src/Validators/ArrayType.php | 2 + src/Validators/ArrayVal.php | 2 + src/Validators/Attributes.php | 4 +- src/Validators/Base64.php | 2 + src/Validators/Between.php | 4 +- src/Validators/BetweenExclusive.php | 4 +- src/Validators/Blank.php | 4 +- src/Validators/BoolType.php | 2 + src/Validators/BoolVal.php | 2 + src/Validators/Bsn.php | 2 + src/Validators/CallableType.php | 2 + src/Validators/Charset.php | 2 + src/Validators/Cnh.php | 2 + src/Validators/Cnpj.php | 2 + src/Validators/Consonant.php | 2 + src/Validators/Control.php | 2 + src/Validators/Countable.php | 2 + src/Validators/CountryCode.php | 2 + src/Validators/Cpf.php | 2 + src/Validators/CreditCard.php | 2 + src/Validators/CurrencyCode.php | 2 + src/Validators/Date.php | 2 + src/Validators/DateTime.php | 2 + src/Validators/DateTimeDiff.php | 2 + src/Validators/Decimal.php | 2 + src/Validators/Digit.php | 2 + src/Validators/Directory.php | 2 + src/Validators/Domain.php | 2 + src/Validators/Each.php | 3 + src/Validators/Email.php | 2 + src/Validators/Emoji.php | 2 + src/Validators/Equals.php | 4 +- src/Validators/Equivalent.php | 4 +- src/Validators/Even.php | 6 +- src/Validators/Executable.php | 2 + src/Validators/Exists.php | 6 +- src/Validators/Extension.php | 2 + src/Validators/Factor.php | 6 +- src/Validators/FalseVal.php | 2 + src/Validators/File.php | 2 + src/Validators/Finite.php | 6 +- src/Validators/FloatType.php | 2 + src/Validators/FloatVal.php | 2 + src/Validators/Format.php | 3 + src/Validators/Formatted.php | 4 +- src/Validators/Graph.php | 2 + src/Validators/GreaterThan.php | 4 +- src/Validators/GreaterThanOrEqual.php | 4 +- src/Validators/Hetu.php | 2 + src/Validators/HexRgbColor.php | 2 + src/Validators/Iban.php | 2 + src/Validators/Identical.php | 7 +- src/Validators/Image.php | 2 + src/Validators/Imei.php | 2 + src/Validators/In.php | 7 +- src/Validators/Infinite.php | 6 +- src/Validators/Instance.php | 4 + src/Validators/IntType.php | 2 + src/Validators/IntVal.php | 2 + src/Validators/Ip.php | 2 + src/Validators/Isbn.php | 2 + src/Validators/IterableType.php | 2 + src/Validators/IterableVal.php | 4 + src/Validators/Json.php | 2 + src/Validators/Key.php | 6 +- src/Validators/KeyExists.php | 4 +- src/Validators/KeyOptional.php | 4 +- src/Validators/KeySet.php | 4 +- src/Validators/LanguageCode.php | 2 + src/Validators/LeapDate.php | 2 + src/Validators/LeapYear.php | 2 + src/Validators/Length.php | 4 +- src/Validators/LessThan.php | 4 +- src/Validators/LessThanOrEqual.php | 4 +- src/Validators/Lowercase.php | 2 + src/Validators/Luhn.php | 2 + src/Validators/MacAddress.php | 2 + src/Validators/Max.php | 4 +- src/Validators/Mimetype.php | 2 + src/Validators/Min.php | 4 +- src/Validators/Multiple.php | 6 +- src/Validators/Named.php | 4 +- src/Validators/Negative.php | 2 + src/Validators/NfeAccessKey.php | 2 + src/Validators/Nif.php | 2 + src/Validators/Nip.php | 2 + src/Validators/NoneOf.php | 4 + src/Validators/Not.php | 7 +- src/Validators/NullOr.php | 10 +- src/Validators/NullType.php | 2 + src/Validators/Number.php | 2 + src/Validators/NumericVal.php | 2 + src/Validators/ObjectType.php | 2 + src/Validators/Odd.php | 6 +- src/Validators/OneOf.php | 3 + src/Validators/Pesel.php | 2 + src/Validators/Phone.php | 2 + src/Validators/Pis.php | 2 + src/Validators/PolishIdCard.php | 2 + src/Validators/PortugueseNif.php | 2 + src/Validators/Positive.php | 6 +- src/Validators/PostalCode.php | 2 + src/Validators/Printable.php | 2 + src/Validators/Property.php | 6 +- src/Validators/PropertyExists.php | 4 +- src/Validators/PropertyOptional.php | 4 +- src/Validators/PublicDomainSuffix.php | 2 + src/Validators/Punct.php | 2 + src/Validators/Readable.php | 2 + src/Validators/Regex.php | 2 + src/Validators/ResourceType.php | 2 + src/Validators/Roman.php | 2 + src/Validators/ScalarVal.php | 2 + src/Validators/Slug.php | 2 + src/Validators/Sorted.php | 2 + src/Validators/Space.php | 2 + src/Validators/Spaced.php | 2 + src/Validators/StringType.php | 2 + src/Validators/StringVal.php | 3 + src/Validators/SubdivisionCode.php | 2 + src/Validators/Subset.php | 2 + src/Validators/SymbolicLink.php | 2 + src/Validators/Templated.php | 4 +- src/Validators/Time.php | 2 + src/Validators/Tld.php | 2 + src/Validators/Trimmed.php | 2 + src/Validators/TrueVal.php | 2 + src/Validators/Undef.php | 4 +- src/Validators/UndefOr.php | 10 +- src/Validators/Unique.php | 2 + src/Validators/Uppercase.php | 2 + src/Validators/Url.php | 2 + src/Validators/Uuid.php | 2 + src/Validators/Version.php | 2 + src/Validators/Vowel.php | 2 + src/Validators/When.php | 3 + src/Validators/Writable.php | 2 + src/Validators/Xdigit.php | 2 + tests/benchmark/PrefixBench.php | 42 -- tests/inference/NarrowingTest.php | 39 ++ tests/inference/assertions/narrowing.php | 475 ++++++++++++++++++ tests/src/Transformers/StubTransformer.php | 23 - .../src/Validators/NonPublic.php | 9 +- tests/unit/ContainerRegistryTest.php | 17 + tests/unit/NamespacedRuleFactoryTest.php | 110 ---- tests/unit/Transformers/PrefixTest.php | 102 ---- tests/unit/ValidatorBuilderTest.php | 4 +- 186 files changed, 1332 insertions(+), 1542 deletions(-) create mode 100644 fluent.neon 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/Mixins/PrefixConstants.php delete mode 100644 src/Mixins/PrefixMap.php delete mode 100644 src/NamespacedValidatorFactory.php delete mode 100644 src/Transformers/Prefix.php delete mode 100644 src/Transformers/Transformer.php delete mode 100644 src/Transformers/ValidatorSpec.php delete mode 100644 src/ValidatorFactory.php delete mode 100644 tests/benchmark/PrefixBench.php create mode 100644 tests/inference/NarrowingTest.php create mode 100644 tests/inference/assertions/narrowing.php delete mode 100644 tests/src/Transformers/StubTransformer.php rename src-dev/CodeGen/CodeGenerator.php => tests/src/Validators/NonPublic.php (57%) delete mode 100644 tests/unit/NamespacedRuleFactoryTest.php delete mode 100644 tests/unit/Transformers/PrefixTest.php diff --git a/.github/workflows/ci-code.yml b/.github/workflows/ci-code.yml index 97d20f988..272b83e6a 100644 --- a/.github/workflows/ci-code.yml +++ b/.github/workflows/ci-code.yml @@ -56,4 +56,5 @@ jobs: - run: composer phpcs - run: composer phpstan + - run: composer inference - run: bin/console lint:mixin diff --git a/REUSE.toml b/REUSE.toml index b4829af26..4dce1d282 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -1,6 +1,6 @@ version = 1 [[annotations]] -path = [ "*.yml", "*.yaml", ".git*", "*.dist", "docs/.pages", "docs/validators/.pages", "composer.json", "composer.lock", "tests/fixtures/*", ".github/*.yml", ".github/actions/**.yml", ".github/workflows/**.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/ISSUE_TEMPLATE/**" ] +path = [ "fluent.neon", "*.yml", "*.yaml", ".git*", "*.dist", "docs/.pages", "docs/validators/.pages", "composer.json", "composer.lock", "tests/fixtures/*", ".github/*.yml", ".github/actions/**.yml", ".github/workflows/**.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/ISSUE_TEMPLATE/**" ] SPDX-FileCopyrightText = "Respect Project Contributors" SPDX-License-Identifier = "MIT" diff --git a/composer.json b/composer.json index 0177f54d2..12cd73a86 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", @@ -36,13 +37,15 @@ "pestphp/pest": "^4.2", "phpbench/phpbench": "^1.4", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.0", + "phpstan/phpstan": "^2.1", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^12.5", "psr/http-message": "^1.0 || ^2.0", "ramsey/uuid": "^4", "respect/coding-standard": "^5.0", + "respect/fluent-analysis": "^2.0", + "respect/fluentgen": "^2.0", "sebastian/diff": "^7.0", "sokil/php-isocodes": "^4.2.1", "sokil/php-isocodes-db-only": "^4.0", @@ -61,6 +64,11 @@ "sokil/php-isocodes": "Enable rules that validate ISO codes", "sokil/php-isocodes-db-only": "Enable rules that validate ISO codes" }, + "extra": { + "phpstan": { + "includes": ["fluent.neon"] + } + }, "autoload": { "psr-4": { "Respect\\Validation\\": "src/" @@ -71,7 +79,8 @@ "psr-4": { "Respect\\Dev\\": "src-dev/", "Respect\\Validation\\": "tests/unit/", - "Respect\\Validation\\Test\\": "tests/src/" + "Respect\\Validation\\Test\\": "tests/src/", + "Respect\\Validation\\Test\\Inference\\": "tests/inference/" } }, "scripts": { @@ -84,6 +93,7 @@ "phpcs": "vendor/bin/phpcs", "phpstan": "vendor/bin/phpstan analyze", "phpunit": "vendor/bin/phpunit --testsuite=unit", + "inference": "vendor/bin/phpunit --testsuite=inference", "smoke-complete": "bin/console smoke-tests:check-complete", "spdx-lint": "bin/console lint:spdx", "qa": [ @@ -92,6 +102,7 @@ "@phpstan", "@phpunit", "@pest", + "@inference", "@docs", "@smoke-complete" ] diff --git a/composer.lock b/composer.lock index 8a5920080..81b98b6da 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": "0e648fd112253307245f3508fa199796", "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.1", + "source": { + "type": "git", + "url": "https://github.com/Respect/Fluent.git", + "reference": "f32c76e37a82a9e63d6fe700a27201534f72da60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/Fluent/zipball/f32c76e37a82a9e63d6fe700a27201534f72da60", + "reference": "f32c76e37a82a9e63d6fe700a27201534f72da60", + "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.1" + }, + "time": "2026-03-26T04:24:51+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.44", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", - "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218", + "reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218", "shasum": "" }, "require": { @@ -3044,7 +3097,7 @@ "type": "github" } ], - "time": "2026-03-17T14:58:32+00:00" + "time": "2026-03-25T17:34:21+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,115 @@ }, "time": "2026-01-19T10:34:07+00:00" }, + { + "name": "respect/fluent-analysis", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/Respect/FluentAnalysis.git", + "reference": "c290b858838b1e71cec2d7f2dec72b0e6b82ed49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/FluentAnalysis/zipball/c290b858838b1e71cec2d7f2dec72b0e6b82ed49", + "reference": "c290b858838b1e71cec2d7f2dec72b0e6b82ed49", + "shasum": "" + }, + "require": { + "php": "^8.5", + "phpstan/phpstan": "^2.1", + "respect/fluent": "^2.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.5", + "respect/coding-standard": "^5.0" + }, + "bin": [ + "bin/fluent-analysis" + ], + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Respect\\FluentAnalysis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "description": "PHPStan extension for Respect/Fluent builder method resolution", + "support": { + "issues": "https://github.com/Respect/FluentAnalysis/issues", + "source": "https://github.com/Respect/FluentAnalysis/tree/2.0.1" + }, + "time": "2026-03-26T04:27:08+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 +4454,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 +4506,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 +4526,7 @@ "type": "tidelift" } ], - "time": "2025-08-12T14:11:56+00:00" + "time": "2026-03-15T07:05:40+00:00" }, { "name": "sebastian/exporter", @@ -4967,32 +5129,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 +5178,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 +5190,7 @@ "type": "tidelift" } ], - "time": "2026-01-25T15:57:07+00:00" + "time": "2026-03-22T17:22:38+00:00" }, { "name": "sokil/php-isocodes", @@ -5264,39 +5426,47 @@ }, { "name": "symfony/console", - "version": "v8.0.7", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", - "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { - "php": ">=8.4", - "symfony/polyfill-mbstring": "^1.0", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.4|^8.0" + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/event-dispatcher": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/lock": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5330,7 +5500,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.7" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -5350,7 +5520,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T14:06:22+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/deprecation-contracts", diff --git a/fluent.neon b/fluent.neon new file mode 100644 index 000000000..e2b468514 --- /dev/null +++ b/fluent.neon @@ -0,0 +1,4 @@ +parameters: + fluent: + builders: + - builder: Respect\Validation\ValidatorBuilder diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 20d17139b..f00329292 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -23,6 +23,10 @@ tests/Pest.php + tests/inference/ + + + tests/inference/ src/Mixins/ diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9a0d4638f..734c91938 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,3 +1,6 @@ +includes: + - fluent.neon + parameters: fileExtensions: - php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 51339be5c..8dae82703 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,6 +14,9 @@ ./tests/feature + + tests/inference/ + 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..6f6f0938b 100644 --- a/src/ContainerRegistry.php +++ b/src/ContainerRegistry.php @@ -14,6 +14,9 @@ use DI\Container; use libphonenumber\PhoneNumberUtil; use Psr\Container\ContainerInterface; +use ReflectionClass; +use Respect\Fluent\Attributes\FluentNamespace; +use Respect\Fluent\FluentFactory; use Respect\StringFormatter\BypassTranslator; use Respect\StringFormatter\Modifier; use Respect\StringFormatter\Modifiers\FormatterModifier; @@ -40,8 +43,6 @@ 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 Symfony\Contracts\Translation\TranslatorInterface; use function DI\autowire; @@ -57,7 +58,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), @@ -67,11 +67,19 @@ public static function createContainer(array $definitions = []): Container 'respect.validation.formatter.full_message' => autowire(NestedListStringFormatter::class), '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'), - )), + 'respect.validation.rule_factory.namespaces' => [], + FluentFactory::class => factory(static function (Container $container) { + $factory = (new ReflectionClass(ValidatorBuilder::class)) + ->getAttributes(FluentNamespace::class)[0] + ->newInstance() + ->factory; + + foreach ($container->get('respect.validation.rule_factory.namespaces') as $namespace) { + $factory = $factory->withNamespace($namespace); + } + + return $factory; + }), Quoter::class => create(CodeQuoter::class)->constructor(120), Handler::class => factory(static function (Container $container) { $handler = CompositeHandler::create(); @@ -101,7 +109,7 @@ public static function createContainer(array $definitions = []): Container $container->get(TranslatorInterface::class), )), ValidatorBuilder::class => factory(static fn(Container $container) => new ValidatorBuilder( - $container->get(ValidatorFactory::class), + $container->get(FluentFactory::class), $container->get(Renderer::class), $container->get('respect.validation.formatter.message'), $container->get('respect.validation.formatter.full_message'), 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/NamespacedValidatorFactory.php b/src/NamespacedValidatorFactory.php deleted file mode 100644 index 369dd8994..000000000 --- a/src/NamespacedValidatorFactory.php +++ /dev/null @@ -1,98 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation; - -use ReflectionClass; -use ReflectionException; -use Respect\Validation\Exceptions\ComponentException; -use Respect\Validation\Exceptions\InvalidClassException; -use Respect\Validation\Transformers\Transformer; -use Respect\Validation\Transformers\ValidatorSpec; - -use function array_merge; -use function Respect\Stringifier\stringify; -use function sprintf; -use function trim; -use function ucfirst; - -final readonly class NamespacedValidatorFactory implements ValidatorFactory -{ - /** @param array $rulesNamespaces */ - public function __construct( - private Transformer $transformer, - private array $rulesNamespaces, - ) { - } - - public function withNamespace(string $rulesNamespace): self - { - return clone ($this, ['rulesNamespaces' => [trim($rulesNamespace, '\\'), ...$this->rulesNamespaces]]); - } - - /** @param array $arguments */ - public function create(string $ruleName, array $arguments = []): Validator - { - return $this->createValidatorSpec($this->transformer->transform(new ValidatorSpec($ruleName, $arguments))); - } - - private function createValidatorSpec(ValidatorSpec $validatorSpec): Validator - { - $validator = $this->createRule($validatorSpec->name, $validatorSpec->arguments); - if ($validatorSpec->wrapper !== null) { - return $this->createRule( - $validatorSpec->wrapper->name, - array_merge($validatorSpec->wrapper->arguments, [$validator]), - ); - } - - return $validator; - } - - /** @param array $arguments */ - private function createRule(string $ruleName, array $arguments = []): Validator - { - $reflection = null; - - foreach ($this->rulesNamespaces as $namespace) { - try { - /** @var class-string $name */ - $name = $namespace . '\\' . ucfirst($ruleName); - $reflection = new ReflectionClass($name); - if (!$reflection->isSubclassOf(Validator::class)) { - throw new InvalidClassException( - sprintf('"%s" must be an instance of "%s"', $name, Validator::class), - ); - } - - if (!$reflection->isInstantiable()) { - throw new InvalidClassException(sprintf('"%s" must be instantiable', $name)); - } - - break; - } catch (ReflectionException) { - continue; - } - } - - if (!$reflection) { - throw new ComponentException(sprintf('"%s" is not a valid rule name', $ruleName)); - } - - try { - return $reflection->newInstanceArgs($arguments); - } catch (ReflectionException) { - throw new InvalidClassException( - sprintf('"%s" could not be instantiated with arguments %s', $ruleName, stringify($arguments)), - ); - } - } -} diff --git a/src/Transformers/Prefix.php b/src/Transformers/Prefix.php deleted file mode 100644 index d6205bb84..000000000 --- a/src/Transformers/Prefix.php +++ /dev/null @@ -1,71 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Transformers; - -use Respect\Validation\Mixins\PrefixMap; - -use function array_keys; -use function array_slice; -use function implode; -use function preg_match; -use function sprintf; - -final class Prefix implements Transformer -{ - private static string|null $regex = null; - - public function transform(ValidatorSpec $validatorSpec): ValidatorSpec - { - $matches = $this->match($validatorSpec); - if ($matches === []) { - return $validatorSpec; - } - - if (!isset(PrefixMap::COMPOSABLE_WITH_ARGUMENT[$matches['prefix']])) { - return new ValidatorSpec( - $matches['suffix'], - $validatorSpec->arguments, - new ValidatorSpec($matches['prefix']), - ); - } - - return new ValidatorSpec( - $matches['suffix'], - array_slice($validatorSpec->arguments, 1), - new ValidatorSpec($matches['prefix'], [$validatorSpec->arguments[0]]), - ); - } - - /** @return array{}|array{prefix: string, suffix: string} */ - private function match(ValidatorSpec $validatorSpec): array - { - if ($validatorSpec->wrapper !== null || isset(PrefixMap::COMPOSABLE[$validatorSpec->name])) { - return []; - } - - preg_match(self::getRegex(), $validatorSpec->name, $matches); - - if ($matches === []) { - return []; - } - - return ['prefix' => $matches['prefix'], 'suffix' => $matches['suffix']]; - } - - private static function getRegex(): string - { - return self::$regex ?? self::$regex = sprintf( - '/^(?%s)(?.+)$/', - implode('|', array_keys(PrefixMap::COMPOSABLE)), - ); - } -} diff --git a/src/Transformers/Transformer.php b/src/Transformers/Transformer.php deleted file mode 100644 index f812487d7..000000000 --- a/src/Transformers/Transformer.php +++ /dev/null @@ -1,17 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Transformers; - -interface Transformer -{ - public function transform(ValidatorSpec $validatorSpec): ValidatorSpec; -} diff --git a/src/Transformers/ValidatorSpec.php b/src/Transformers/ValidatorSpec.php deleted file mode 100644 index 33220a71c..000000000 --- a/src/Transformers/ValidatorSpec.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Transformers; - -final readonly class ValidatorSpec -{ - /** @param array $arguments */ - public function __construct( - public string $name, - public array $arguments = [], - public ValidatorSpec|null $wrapper = null, - ) { - } -} diff --git a/src/ValidatorBuilder.php b/src/ValidatorBuilder.php index 5c279989f..9a82dfa72 100644 --- a/src/ValidatorBuilder.php +++ b/src/ValidatorBuilder.php @@ -12,12 +12,22 @@ namespace Respect\Validation; +use Respect\Fluent\Attributes\AssuranceAssertion; +use Respect\Fluent\Attributes\AssuranceParameter; +use Respect\Fluent\Attributes\FluentNamespace; +use Respect\Fluent\Builders\Append; +use Respect\Fluent\Factories\ComposingLookup; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\FluentFactory; +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,14 +41,15 @@ use function is_string; /** @mixin Builder */ -final readonly class ValidatorBuilder implements Nameable, ShortCircuitable +#[FluentNamespace(new ComposingLookup( + new NamespaceLookup(new Ucfirst(), Validator::class, 'Respect\\Validation\\Validators'), + new ComposableMap(PrefixConstants::COMPOSABLE, PrefixConstants::COMPOSABLE_WITH_ARGUMENT), +))] +final readonly class ValidatorBuilder extends Append implements Nameable, ShortCircuitable { - /** @var array */ - private array $validators; - /** @param array $ignoredBacktracePaths */ public function __construct( - private ValidatorFactory $validatorFactory, + FluentFactory $factory, private Renderer $renderer, private StringFormatter $mainMessageFormatter, private StringFormatter $fullMessageFormatter, @@ -47,7 +58,7 @@ public function __construct( private array $ignoredBacktracePaths, Validator ...$validators, ) { - $this->validators = $validators; + parent::__construct($factory, ...$validators); } public static function init(Validator ...$validators): self @@ -56,15 +67,17 @@ public static function init(Validator ...$validators): self return ContainerRegistry::getContainer()->get(self::class); } - return ContainerRegistry::getContainer()->get(self::class)->with(...$validators); + return ContainerRegistry::getContainer()->get(self::class)->attach(...$validators); } public function evaluate(mixed $input): Result { - $validator = match (count($this->validators)) { + $validators = $this->getValidators(); + + $validator = match (count($validators)) { 0 => throw new ComponentException('No validators have been added.'), - 1 => current($this->validators), - default => new AllOf(...$this->validators), + 1 => current($validators), + default => new AllOf(...$validators), }; return $validator->evaluate($input); @@ -72,7 +85,9 @@ public function evaluate(mixed $input): Result public function evaluateShortCircuit(mixed $input): Result { - return (new ShortCircuit(...$this->validators))->evaluate($input); + $validators = $this->getValidators(); + + return (new ShortCircuit(...$validators))->evaluate($input); } /** @param array|string|null $template */ @@ -81,38 +96,46 @@ 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); } - public function with(Validator $validator, Validator ...$validators): self - { - return clone ($this, ['validators' => [...$this->validators, $validator, ...$validators]]); - } - /** @return array */ public function getValidators(): array { - return $this->validators; + return $this->getNodes(); } public function getName(): Name|null { - if (count($this->validators) === 1 && current($this->validators) instanceof Nameable) { - return current($this->validators)->getName(); + $validators = $this->getNodes(); + + if (count($validators) === 1 && current($validators) instanceof Nameable) { + return current($validators)->getName(); } return null; @@ -153,14 +176,8 @@ private function throwOnFailure(Result $result, array|callable|Throwable|string| } /** @param array $arguments */ - public static function __callStatic(string $ruleName, array $arguments): self + public static function __callStatic(string $ruleName, array $arguments): static { return self::init()->__call($ruleName, $arguments); } - - /** @param array $arguments */ - public function __call(string $ruleName, array $arguments): self - { - return $this->with($this->validatorFactory->create($ruleName, $arguments)); - } } diff --git a/src/ValidatorFactory.php b/src/ValidatorFactory.php deleted file mode 100644 index 24c25473e..000000000 --- a/src/ValidatorFactory.php +++ /dev/null @@ -1,18 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation; - -interface ValidatorFactory -{ - /** @param array $arguments */ - public function create(string $ruleName, array $arguments = []): Validator; -} diff --git a/src/Validators/All.php b/src/Validators/All.php index 6a7d728b5..4593aa1e1 100644 --- a/src/Validators/All.php +++ b/src/Validators/All.php @@ -15,7 +15,9 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceFrom; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Path; @@ -23,9 +25,10 @@ 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')] +#[Assurance(from: AssuranceFrom::Elements)] final class All extends FilteredArray implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/AllOf.php b/src/Validators/AllOf.php index 75701b6cc..88470c9bd 100644 --- a/src/Validators/AllOf.php +++ b/src/Validators/AllOf.php @@ -15,6 +15,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -38,6 +40,7 @@ '{{subject}} must pass all the rules', self::TEMPLATE_ALL, )] +#[Assurance(compose: AssuranceCompose::Intersect)] final class AllOf extends LogicalComposite implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/Alnum.php b/src/Validators/Alnum.php index d734082b1..7c87a508b 100644 --- a/src/Validators/Alnum.php +++ b/src/Validators/Alnum.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -31,6 +32,7 @@ '{{subject}} must not consist only of letters (a-z), digits (0-9), or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Alnum extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Alpha.php b/src/Validators/Alpha.php index 986554c6a..90bbc846d 100644 --- a/src/Validators/Alpha.php +++ b/src/Validators/Alpha.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -31,6 +32,7 @@ '{{subject}} must not consist only of letters (a-z) or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Alpha extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/AnyOf.php b/src/Validators/AnyOf.php index de9b8aadd..9d061b3d1 100644 --- a/src/Validators/AnyOf.php +++ b/src/Validators/AnyOf.php @@ -15,6 +15,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -30,6 +32,7 @@ '{{subject}} must pass at least one of the rules', '{{subject}} must pass at least one of the rules', )] +#[Assurance(compose: AssuranceCompose::Union)] final class AnyOf extends LogicalComposite implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/ArrayType.php b/src/Validators/ArrayType.php index 9ada2804e..a4a9a1df0 100644 --- a/src/Validators/ArrayType.php +++ b/src/Validators/ArrayType.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be an array', '{{subject}} must not be an array', )] +#[Assurance(type: 'array')] final class ArrayType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/ArrayVal.php b/src/Validators/ArrayVal.php index cede9834c..f12267328 100644 --- a/src/Validators/ArrayVal.php +++ b/src/Validators/ArrayVal.php @@ -18,6 +18,7 @@ use ArrayAccess; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SimpleXMLElement; @@ -29,6 +30,7 @@ '{{subject}} must be an array', '{{subject}} must not be an array', )] +#[Assurance(type: ['array', ArrayAccess::class, SimpleXMLElement::class])] final class ArrayVal extends Simple { public function isValid(mixed $input): bool 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/Base64.php b/src/Validators/Base64.php index 560857a02..21cf3543e 100644 --- a/src/Validators/Base64.php +++ b/src/Validators/Base64.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a base64-encoded string', '{{subject}} must not be a base64-encoded string', )] +#[Assurance(type: 'string')] final class Base64 extends Simple { public function isValid(mixed $input): bool 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/BoolType.php b/src/Validators/BoolType.php index 49626f90b..4e7c1e1af 100644 --- a/src/Validators/BoolType.php +++ b/src/Validators/BoolType.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a boolean', '{{subject}} must not be a boolean', )] +#[Assurance(type: 'bool')] final class BoolType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/BoolVal.php b/src/Validators/BoolVal.php index 277ca2409..24b4b06a7 100644 --- a/src/Validators/BoolVal.php +++ b/src/Validators/BoolVal.php @@ -18,6 +18,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -32,6 +33,7 @@ '{{subject}} must be a boolean', '{{subject}} must not be a boolean', )] +#[Assurance(type: 'bool')] final class BoolVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Bsn.php b/src/Validators/Bsn.php index 7d4ac1838..b357c0979 100644 --- a/src/Validators/Bsn.php +++ b/src/Validators/Bsn.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a BSN', '{{subject}} must not be a BSN', )] +#[Assurance(type: 'string')] final class Bsn extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/CallableType.php b/src/Validators/CallableType.php index 04dc72cb0..3a543eaf7 100644 --- a/src/Validators/CallableType.php +++ b/src/Validators/CallableType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be a callable function', '{{subject}} must not be a callable function', )] +#[Assurance(type: 'callable')] final class CallableType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Charset.php b/src/Validators/Charset.php index 7ac4c01d3..a4cbb77b4 100644 --- a/src/Validators/Charset.php +++ b/src/Validators/Charset.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -33,6 +34,7 @@ '{{subject}} must consist only of characters from the {{charset|list:or}} character-set', '{{subject}} must not consist only of characters from the {{charset|list:or}} character-set', )] +#[Assurance(type: 'string')] final readonly class Charset implements Validator { /** @var non-empty-array */ diff --git a/src/Validators/Cnh.php b/src/Validators/Cnh.php index 50b9a5664..eef2cd35b 100644 --- a/src/Validators/Cnh.php +++ b/src/Validators/Cnh.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -30,6 +31,7 @@ '{{subject}} must be a CNH', '{{subject}} must not be a CNH', )] +#[Assurance(type: 'string')] final class Cnh extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Cnpj.php b/src/Validators/Cnpj.php index 3cfc56918..d226baee8 100644 --- a/src/Validators/Cnpj.php +++ b/src/Validators/Cnpj.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -34,6 +35,7 @@ '{{subject}} must be a CNPJ', '{{subject}} must not be a CNPJ', )] +#[Assurance(type: 'string')] final class Cnpj extends Simple { private const int BASE_ASCII = 48; diff --git a/src/Validators/Consonant.php b/src/Validators/Consonant.php index da517afb5..fd5d2be35 100644 --- a/src/Validators/Consonant.php +++ b/src/Validators/Consonant.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -33,6 +34,7 @@ '{{subject}} must not consist only of consonants or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Consonant extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Control.php b/src/Validators/Control.php index dc6dea0db..da9c0f618 100644 --- a/src/Validators/Control.php +++ b/src/Validators/Control.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of control characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Control extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Countable.php b/src/Validators/Countable.php index 7b75be4f0..e61d0cf28 100644 --- a/src/Validators/Countable.php +++ b/src/Validators/Countable.php @@ -20,6 +20,7 @@ use Attribute; use Countable as CountableInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -30,6 +31,7 @@ '{{subject}} must be countable', '{{subject}} must not be countable', )] +#[Assurance(type: ['array', CountableInterface::class])] final class Countable extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/CountryCode.php b/src/Validators/CountryCode.php index 93894f565..6a53f68cf 100644 --- a/src/Validators/CountryCode.php +++ b/src/Validators/CountryCode.php @@ -19,6 +19,7 @@ use Attribute; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -35,6 +36,7 @@ '{{subject}} must be a country code', '{{subject}} must not be a country code', )] +#[Assurance(type: 'string')] final readonly class CountryCode implements Validator { private Countries $countries; diff --git a/src/Validators/Cpf.php b/src/Validators/Cpf.php index 1e2f69f81..2fd72c869 100644 --- a/src/Validators/Cpf.php +++ b/src/Validators/Cpf.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -33,6 +34,7 @@ '{{subject}} must be a CPF', '{{subject}} must not be a CPF', )] +#[Assurance(type: 'string')] final class Cpf extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/CreditCard.php b/src/Validators/CreditCard.php index 23ed28857..fd429fe02 100644 --- a/src/Validators/CreditCard.php +++ b/src/Validators/CreditCard.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -41,6 +42,7 @@ '{{subject}} must not be a {{brand|raw}} credit card number', self::TEMPLATE_BRANDED, )] +#[Assurance(type: 'string')] final readonly class CreditCard implements Validator { public const string TEMPLATE_BRANDED = '__branded__'; diff --git a/src/Validators/CurrencyCode.php b/src/Validators/CurrencyCode.php index 74af78c08..f4314e98e 100644 --- a/src/Validators/CurrencyCode.php +++ b/src/Validators/CurrencyCode.php @@ -16,6 +16,7 @@ use Attribute; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -31,6 +32,7 @@ '{{subject}} must be a currency code', '{{subject}} must not be a currency code', )] +#[Assurance(type: 'string')] final readonly class CurrencyCode implements Validator { private Currencies $currencies; diff --git a/src/Validators/Date.php b/src/Validators/Date.php index 151ee9b0a..d9f391e56 100644 --- a/src/Validators/Date.php +++ b/src/Validators/Date.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; @@ -33,6 +34,7 @@ '{{subject}} must be a date in the {{sample}} format', '{{subject}} must not be a date in the {{sample}} format', )] +#[Assurance(type: 'string')] final readonly class Date implements Validator { use CanValidateDateTime; diff --git a/src/Validators/DateTime.php b/src/Validators/DateTime.php index 8cef8308e..f9ef97590 100644 --- a/src/Validators/DateTime.php +++ b/src/Validators/DateTime.php @@ -18,6 +18,7 @@ use Attribute; use DateTimeInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -38,6 +39,7 @@ '{{subject}} must not be a date/time in the {{sample}} format', self::TEMPLATE_FORMAT, )] +#[Assurance(type: ['string', DateTimeInterface::class])] final class DateTime implements Validator { use CanValidateDateTime; diff --git a/src/Validators/DateTimeDiff.php b/src/Validators/DateTimeDiff.php index 194a117d1..96373a362 100644 --- a/src/Validators/DateTimeDiff.php +++ b/src/Validators/DateTimeDiff.php @@ -14,6 +14,7 @@ use Attribute; use DateTimeImmutable; use DateTimeInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; @@ -44,6 +45,7 @@ 'For comparison with {{now|raw}}, {{subject}} must not be a datetime in the format {{sample|raw}}', self::TEMPLATE_WRONG_FORMAT, )] +#[Assurance(type: ['string', DateTimeInterface::class])] final readonly class DateTimeDiff implements Validator { use CanValidateDateTime; diff --git a/src/Validators/Decimal.php b/src/Validators/Decimal.php index 424109587..238275b4b 100644 --- a/src/Validators/Decimal.php +++ b/src/Validators/Decimal.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -28,6 +29,7 @@ '{{subject}} must have {{decimals}} decimal places', '{{subject}} must not have {{decimals}} decimal places', )] +#[Assurance(type: 'string')] final readonly class Decimal implements Validator { public function __construct( diff --git a/src/Validators/Digit.php b/src/Validators/Digit.php index 5d432749b..213cbcf04 100644 --- a/src/Validators/Digit.php +++ b/src/Validators/Digit.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -31,6 +32,7 @@ '{{subject}} must not consist only of digits (0-9) or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Digit extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Directory.php b/src/Validators/Directory.php index 4437ac4f0..f3987dca3 100644 --- a/src/Validators/Directory.php +++ b/src/Validators/Directory.php @@ -16,6 +16,7 @@ use Attribute; use Directory as NativeDirectory; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -28,6 +29,7 @@ '{{subject}} must be an accessible existing directory', '{{subject}} must not be an accessible existing directory', )] +#[Assurance(type: ['string', SplFileInfo::class])] final class Directory extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Domain.php b/src/Validators/Domain.php index e4ec03bc0..5053fe80a 100644 --- a/src/Validators/Domain.php +++ b/src/Validators/Domain.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -30,6 +31,7 @@ '{{subject}} must be an internet domain', '{{subject}} must not be an internet domain', )] +#[Assurance(type: 'string')] final class Domain implements Validator { private readonly Validator $genericRule; diff --git a/src/Validators/Each.php b/src/Validators/Each.php index 43d24f04f..c3aa48ebd 100644 --- a/src/Validators/Each.php +++ b/src/Validators/Each.php @@ -18,6 +18,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceFrom; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Path; @@ -32,6 +34,7 @@ 'Each item in {{subject}} must be valid', 'Each item in {{subject}} must be invalid', )] +#[Assurance(from: AssuranceFrom::Elements)] final class Each extends FilteredArray implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/Email.php b/src/Validators/Email.php index 5877204e5..bed560404 100644 --- a/src/Validators/Email.php +++ b/src/Validators/Email.php @@ -21,6 +21,7 @@ use Attribute; use Egulias\EmailValidator\EmailValidator; use Egulias\EmailValidator\Validation\RFCValidation; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -36,6 +37,7 @@ '{{subject}} must be an email address', '{{subject}} must not be an email address', )] +#[Assurance(type: 'string')] final class Email extends Simple { private readonly EmailValidator|null $validator; diff --git a/src/Validators/Emoji.php b/src/Validators/Emoji.php index fa258c4c1..141d084bb 100644 --- a/src/Validators/Emoji.php +++ b/src/Validators/Emoji.php @@ -12,6 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -23,6 +24,7 @@ '{{subject}} must be an emoji', '{{subject}} must not be an emoji', )] +#[Assurance(type: 'string')] final class Emoji extends Simple { private const string REGEX = <<<'REGEX' 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..4fea0f782 100644 --- a/src/Validators/Even.php +++ b/src/Validators/Even.php @@ -18,7 +18,8 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,12 +27,13 @@ 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', '{{subject}} must be an odd number', )] +#[Assurance(type: 'int')] final class Even extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Executable.php b/src/Validators/Executable.php index 6e056f740..b1e4b965b 100644 --- a/src/Validators/Executable.php +++ b/src/Validators/Executable.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -27,6 +28,7 @@ '{{subject}} must be an accessible existing executable file', '{{subject}} must not be an accessible existing executable file', )] +#[Assurance(type: ['string', SplFileInfo::class])] final class Executable extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Exists.php b/src/Validators/Exists.php index dee5bab98..3d7ce0def 100644 --- a/src/Validators/Exists.php +++ b/src/Validators/Exists.php @@ -15,7 +15,8 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -23,12 +24,13 @@ 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', '{{subject}} must not be an existing file', )] +#[Assurance(type: 'string')] final class Exists extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Extension.php b/src/Validators/Extension.php index 317fef0c6..7d4e7b742 100644 --- a/src/Validators/Extension.php +++ b/src/Validators/Extension.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -28,6 +29,7 @@ '{{subject}} must have the {{extension}} extension', '{{subject}} must not have the {{extension}} extension', )] +#[Assurance(type: ['string', SplFileInfo::class])] final readonly class Extension implements Validator { public function __construct( diff --git a/src/Validators/Factor.php b/src/Validators/Factor.php index bfdcdcacc..c1a501410 100644 --- a/src/Validators/Factor.php +++ b/src/Validators/Factor.php @@ -14,7 +14,8 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -24,12 +25,13 @@ 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}}', '{{subject}} must not be a factor of {{dividend|raw}}', )] +#[Assurance(type: 'int')] final readonly class Factor implements Validator { public function __construct( diff --git a/src/Validators/FalseVal.php b/src/Validators/FalseVal.php index e5f1fc76a..c09599b6a 100644 --- a/src/Validators/FalseVal.php +++ b/src/Validators/FalseVal.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,6 +27,7 @@ '{{subject}} must evaluate to `false`', '{{subject}} must not evaluate to `false`', )] +#[Assurance(type: 'false')] final class FalseVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/File.php b/src/Validators/File.php index 2d48d3d55..e0625d7d7 100644 --- a/src/Validators/File.php +++ b/src/Validators/File.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -27,6 +28,7 @@ '{{subject}} must be an accessible existing file', '{{subject}} must not be an accessible existing file', )] +#[Assurance(type: ['string', SplFileInfo::class])] final class File extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Finite.php b/src/Validators/Finite.php index 9c917861b..dcd835a9e 100644 --- a/src/Validators/Finite.php +++ b/src/Validators/Finite.php @@ -15,19 +15,21 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +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', '{{subject}} must not be a finite number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Finite extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/FloatType.php b/src/Validators/FloatType.php index a94de207c..f87feb076 100644 --- a/src/Validators/FloatType.php +++ b/src/Validators/FloatType.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a float', '{{subject}} must not be a float', )] +#[Assurance(type: 'float')] final class FloatType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/FloatVal.php b/src/Validators/FloatVal.php index 515327314..3f034baca 100644 --- a/src/Validators/FloatVal.php +++ b/src/Validators/FloatVal.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a floating-point number', '{{subject}} must not be a floating-point number', )] +#[Assurance(type: 'int|float|numeric-string')] final class FloatVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Format.php b/src/Validators/Format.php index 99a35a975..c56e19332 100644 --- a/src/Validators/Format.php +++ b/src/Validators/Format.php @@ -12,16 +12,19 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\StringFormatter\Formatter; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; +use Stringable; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be formatted as {{formatted}}', '{{subject}} must not be formatted as {{formatted}}', )] +#[Assurance(type: ['scalar', Stringable::class])] final readonly class Format implements Validator { public function __construct( 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/Graph.php b/src/Validators/Graph.php index 4c0c40de5..e36bed049 100644 --- a/src/Validators/Graph.php +++ b/src/Validators/Graph.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of printable non-spacing characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Graph extends FilteredString { protected function isValid(string $input): bool 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/Hetu.php b/src/Validators/Hetu.php index c3025f403..c9203f164 100644 --- a/src/Validators/Hetu.php +++ b/src/Validators/Hetu.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be a Finnish personal identity code', '{{subject}} must not be a Finnish personal identity code', )] +#[Assurance(type: 'string')] final class Hetu extends Simple { use CanValidateDateTime; diff --git a/src/Validators/HexRgbColor.php b/src/Validators/HexRgbColor.php index 5203ee073..c1b85b8ff 100644 --- a/src/Validators/HexRgbColor.php +++ b/src/Validators/HexRgbColor.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; @@ -21,6 +22,7 @@ '{{subject}} must be a hex RGB color', '{{subject}} must not be a hex RGB color', )] +#[Assurance(type: 'string')] final class HexRgbColor extends Envelope { public function __construct() diff --git a/src/Validators/Iban.php b/src/Validators/Iban.php index 5f0d5eeec..b28fbedb9 100644 --- a/src/Validators/Iban.php +++ b/src/Validators/Iban.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be an IBAN', '{{subject}} must not be an IBAN', )] +#[Assurance(type: 'string')] final class Iban extends Simple { private const array COUNTRIES_LENGTHS = [ diff --git a/src/Validators/Identical.php b/src/Validators/Identical.php index 25380531e..ebfb1e444 100644 --- a/src/Validators/Identical.php +++ b/src/Validators/Identical.php @@ -15,17 +15,20 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceFrom; +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}}', '{{subject}} must not be identical to {{compareTo}}', )] +#[Assurance(from: AssuranceFrom::Value)] final readonly class Identical implements Validator { public function __construct( diff --git a/src/Validators/Image.php b/src/Validators/Image.php index da81a7da0..c5f24014f 100644 --- a/src/Validators/Image.php +++ b/src/Validators/Image.php @@ -15,6 +15,7 @@ use Attribute; use finfo; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -31,6 +32,7 @@ '{{subject}} must be an accessible existing image file', '{{subject}} must not be an accessible existing image file', )] +#[Assurance(type: 'string')] final readonly class Image implements Validator { public function evaluate(mixed $input): Result diff --git a/src/Validators/Imei.php b/src/Validators/Imei.php index 9f8b4ae6a..487eee327 100644 --- a/src/Validators/Imei.php +++ b/src/Validators/Imei.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be an IMEI number', '{{subject}} must not be an IMEI number', )] +#[Assurance(type: 'string')] final class Imei extends Simple { private const int IMEI_SIZE = 15; diff --git a/src/Validators/In.php b/src/Validators/In.php index 53d46d9b7..d6870e162 100644 --- a/src/Validators/In.php +++ b/src/Validators/In.php @@ -16,7 +16,9 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceFrom; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -25,12 +27,13 @@ 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}}', '{{subject}} must not be in {{haystack}}', )] +#[Assurance(from: AssuranceFrom::Member)] final readonly class In implements Validator { public function __construct( diff --git a/src/Validators/Infinite.php b/src/Validators/Infinite.php index 34bcc67cd..eaff0a239 100644 --- a/src/Validators/Infinite.php +++ b/src/Validators/Infinite.php @@ -15,19 +15,21 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +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', '{{subject}} must not be an infinite number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Infinite extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Instance.php b/src/Validators/Instance.php index 5c63858fa..a924198e3 100644 --- a/src/Validators/Instance.php +++ b/src/Validators/Instance.php @@ -15,6 +15,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceParameter; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -24,10 +26,12 @@ '{{subject}} must be an instance of {{class|quote}}', '{{subject}} must not be an instance of {{class|quote}}', )] +#[Assurance] final readonly class Instance implements Validator { /** @param class-string $class */ public function __construct( + #[AssuranceParameter] private string $class, ) { } diff --git a/src/Validators/IntType.php b/src/Validators/IntType.php index 85ad69e57..16ccf8461 100644 --- a/src/Validators/IntType.php +++ b/src/Validators/IntType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be an integer', '{{subject}} must not be an integer', )] +#[Assurance(type: 'int')] final class IntType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/IntVal.php b/src/Validators/IntVal.php index d20783cc8..94b52b226 100644 --- a/src/Validators/IntVal.php +++ b/src/Validators/IntVal.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -32,6 +33,7 @@ '{{subject}} must be an integer', '{{subject}} must not be an integer', )] +#[Assurance(type: 'int|numeric-string')] final class IntVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Ip.php b/src/Validators/Ip.php index f4c26e2b9..3c0971778 100644 --- a/src/Validators/Ip.php +++ b/src/Validators/Ip.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -49,6 +50,7 @@ '{{subject}} must not be an IP address in the {{range|raw}} range', self::TEMPLATE_NETWORK_RANGE, )] +#[Assurance(type: 'string')] final class Ip implements Validator { public const string TEMPLATE_NETWORK_RANGE = '__network_range__'; diff --git a/src/Validators/Isbn.php b/src/Validators/Isbn.php index a9c8716be..eb147a0ac 100644 --- a/src/Validators/Isbn.php +++ b/src/Validators/Isbn.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,6 +27,7 @@ '{{subject}} must be an ISBN', '{{subject}} must not be an ISBN', )] +#[Assurance(type: 'string')] final class Isbn extends Simple { /** @see https://howtodoinjava.com/regex/java-regex-validate-international-standard-book-number-isbns */ diff --git a/src/Validators/IterableType.php b/src/Validators/IterableType.php index 249b98359..d6c45650d 100644 --- a/src/Validators/IterableType.php +++ b/src/Validators/IterableType.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be iterable', '{{subject}} must not iterable', )] +#[Assurance(type: 'iterable')] final class IterableType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/IterableVal.php b/src/Validators/IterableVal.php index a8b39f7b3..d7b80428e 100644 --- a/src/Validators/IterableVal.php +++ b/src/Validators/IterableVal.php @@ -16,15 +16,19 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\CanValidateIterable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; +use stdClass; +use Traversable; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be iterable', '{{subject}} must not be iterable', )] +#[Assurance(type: ['array', stdClass::class, Traversable::class])] final class IterableVal extends Simple { use CanValidateIterable; diff --git a/src/Validators/Json.php b/src/Validators/Json.php index 643f80052..9e9eea5f5 100644 --- a/src/Validators/Json.php +++ b/src/Validators/Json.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be a JSON string', '{{subject}} must not be a JSON string', )] +#[Assurance(type: 'string')] final class Json extends Simple { public function isValid(mixed $input): bool 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/LanguageCode.php b/src/Validators/LanguageCode.php index 0199c1ae9..ed28ea9ce 100644 --- a/src/Validators/LanguageCode.php +++ b/src/Validators/LanguageCode.php @@ -15,6 +15,7 @@ use Attribute; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -31,6 +32,7 @@ '{{subject}} must be a language code', '{{subject}} must not be a language code', )] +#[Assurance(type: 'string')] final readonly class LanguageCode implements Validator { private Languages $languages; diff --git a/src/Validators/LeapDate.php b/src/Validators/LeapDate.php index 08c608826..abd433e80 100644 --- a/src/Validators/LeapDate.php +++ b/src/Validators/LeapDate.php @@ -18,6 +18,7 @@ use Attribute; use DateTimeImmutable; use DateTimeInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -28,6 +29,7 @@ '{{subject}} must be a leap date', '{{subject}} must not be a leap date', )] +#[Assurance(type: 'string')] final class LeapDate extends Simple { public function __construct( diff --git a/src/Validators/LeapYear.php b/src/Validators/LeapYear.php index b0fafefaf..165ca58b2 100644 --- a/src/Validators/LeapYear.php +++ b/src/Validators/LeapYear.php @@ -17,6 +17,7 @@ use Attribute; use DateTimeInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be a leap year', '{{subject}} must not be a leap year', )] +#[Assurance(type: 'int|string')] final class LeapYear extends Simple { public function isValid(mixed $input): bool 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/Lowercase.php b/src/Validators/Lowercase.php index dddd38ce3..d7031879d 100644 --- a/src/Validators/Lowercase.php +++ b/src/Validators/Lowercase.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must consist only of lowercase letters', '{{subject}} must not consist only of lowercase letters', )] +#[Assurance(type: 'string')] final class Lowercase extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Luhn.php b/src/Validators/Luhn.php index f77197f80..2e8f074d0 100644 --- a/src/Validators/Luhn.php +++ b/src/Validators/Luhn.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -28,6 +29,7 @@ '{{subject}} must be a Luhn number', '{{subject}} must not be a Luhn number', )] +#[Assurance(type: 'string')] final class Luhn extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/MacAddress.php b/src/Validators/MacAddress.php index 158d07858..40f610005 100644 --- a/src/Validators/MacAddress.php +++ b/src/Validators/MacAddress.php @@ -18,6 +18,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a MAC address', '{{subject}} must not be a MAC address', )] +#[Assurance(type: 'string')] final class MacAddress extends Simple { public function isValid(mixed $input): bool 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/Mimetype.php b/src/Validators/Mimetype.php index affef309c..45fd4a753 100644 --- a/src/Validators/Mimetype.php +++ b/src/Validators/Mimetype.php @@ -14,6 +14,7 @@ use Attribute; use finfo; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -29,6 +30,7 @@ '{{subject}} must have the {{mimetype}} MIME type', '{{subject}} must not have the {{mimetype}} MIME type', )] +#[Assurance(type: 'string')] final readonly class Mimetype implements Validator { public function __construct( 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..afbddd97a 100644 --- a/src/Validators/Multiple.php +++ b/src/Validators/Multiple.php @@ -17,17 +17,19 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +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}}', '{{subject}} must not be a multiple of {{multipleOf}}', )] +#[Assurance(type: 'int')] final readonly class Multiple implements Validator { public function __construct( 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/Negative.php b/src/Validators/Negative.php index 6eebb5ee4..f03ea6ae5 100644 --- a/src/Validators/Negative.php +++ b/src/Validators/Negative.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a negative number', '{{subject}} must not be a negative number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Negative extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/NfeAccessKey.php b/src/Validators/NfeAccessKey.php index 33dcc0139..d99d5c38e 100644 --- a/src/Validators/NfeAccessKey.php +++ b/src/Validators/NfeAccessKey.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a NFe access key', '{{subject}} must not be a NFe access key', )] +#[Assurance(type: 'string')] final class NfeAccessKey extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Nif.php b/src/Validators/Nif.php index ef01ae7b8..169b7f76d 100644 --- a/src/Validators/Nif.php +++ b/src/Validators/Nif.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be a NIF', '{{subject}} must not be a NIF', )] +#[Assurance(type: 'string')] final class Nif extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Nip.php b/src/Validators/Nip.php index fa015a0a0..253afc2c7 100644 --- a/src/Validators/Nip.php +++ b/src/Validators/Nip.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be a Polish VAT identification number', '{{subject}} must not be a Polish VAT identification number', )] +#[Assurance(type: 'string')] final class Nip extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/NoneOf.php b/src/Validators/NoneOf.php index e0838ce87..1f309db98 100644 --- a/src/Validators/NoneOf.php +++ b/src/Validators/NoneOf.php @@ -15,6 +15,9 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; +use Respect\Fluent\Attributes\AssuranceModifier; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -34,6 +37,7 @@ '{{subject}} must pass all the rules', self::TEMPLATE_ALL, )] +#[Assurance(compose: AssuranceCompose::Union, modifier: AssuranceModifier::Exclude)] final class NoneOf extends LogicalComposite implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/Not.php b/src/Validators/Not.php index 1026bb6c6..2363fce30 100644 --- a/src/Validators/Not.php +++ b/src/Validators/Not.php @@ -18,12 +18,15 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceModifier; +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)] +#[Assurance(modifier: AssuranceModifier::Exclude)] final readonly class Not implements Validator { public function __construct( diff --git a/src/Validators/NullOr.php b/src/Validators/NullOr.php index 9b02f1500..a026090ba 100644 --- a/src/Validators/NullOr.php +++ b/src/Validators/NullOr.php @@ -14,19 +14,25 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceModifier; +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', 'and must not be null', )] +#[Assurance(modifier: AssuranceModifier::Nullable)] final readonly class NullOr implements Validator { public function __construct( diff --git a/src/Validators/NullType.php b/src/Validators/NullType.php index d6c15df8c..da29bd2ee 100644 --- a/src/Validators/NullType.php +++ b/src/Validators/NullType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -22,6 +23,7 @@ '{{subject}} must be null', '{{subject}} must not be null', )] +#[Assurance(type: 'null')] final class NullType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Number.php b/src/Validators/Number.php index eac0169fa..220a88c70 100644 --- a/src/Validators/Number.php +++ b/src/Validators/Number.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -28,6 +29,7 @@ '{{subject}} must be a number', '{{subject}} must not be a number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Number extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/NumericVal.php b/src/Validators/NumericVal.php index 5a5f27253..94d103981 100644 --- a/src/Validators/NumericVal.php +++ b/src/Validators/NumericVal.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be numeric', '{{subject}} must not be numeric', )] +#[Assurance(type: 'int|float|numeric-string')] final class NumericVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/ObjectType.php b/src/Validators/ObjectType.php index 5c9dfccaa..5ac989ac6 100644 --- a/src/Validators/ObjectType.php +++ b/src/Validators/ObjectType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be an object', '{{subject}} must not be an object', )] +#[Assurance(type: 'object')] final class ObjectType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Odd.php b/src/Validators/Odd.php index bc0331fd7..ec252b8c9 100644 --- a/src/Validators/Odd.php +++ b/src/Validators/Odd.php @@ -17,7 +17,8 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,12 +27,13 @@ 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', '{{subject}} must be an even number', )] +#[Assurance(type: 'int')] final class Odd extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/OneOf.php b/src/Validators/OneOf.php index 11c871130..1d42c6196 100644 --- a/src/Validators/OneOf.php +++ b/src/Validators/OneOf.php @@ -16,6 +16,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -40,6 +42,7 @@ '{{subject}} must pass only one of the rules', self::TEMPLATE_MORE_THAN_ONE, )] +#[Assurance(compose: AssuranceCompose::Union)] final class OneOf extends LogicalComposite implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/Pesel.php b/src/Validators/Pesel.php index 67f2421bc..936f6f6f4 100644 --- a/src/Validators/Pesel.php +++ b/src/Validators/Pesel.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a PESEL', '{{subject}} must not be a PESEL', )] +#[Assurance(type: 'string')] final class Pesel extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Phone.php b/src/Validators/Phone.php index 78f8f3881..95452d907 100644 --- a/src/Validators/Phone.php +++ b/src/Validators/Phone.php @@ -22,6 +22,7 @@ use libphonenumber\NumberParseException; use libphonenumber\PhoneNumberUtil; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -43,6 +44,7 @@ '{{subject}} must not be a phone number for country {{countryName|trans}}', self::TEMPLATE_FOR_COUNTRY, )] +#[Assurance(type: 'string')] final class Phone implements Validator { public const string TEMPLATE_FOR_COUNTRY = '__for_country__'; diff --git a/src/Validators/Pis.php b/src/Validators/Pis.php index 9ddfbdbf2..7e3811577 100644 --- a/src/Validators/Pis.php +++ b/src/Validators/Pis.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be a PIS number', '{{subject}} must not be a PIS number', )] +#[Assurance(type: 'string')] final class Pis extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/PolishIdCard.php b/src/Validators/PolishIdCard.php index 602a1267a..636204a14 100644 --- a/src/Validators/PolishIdCard.php +++ b/src/Validators/PolishIdCard.php @@ -12,6 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a Polish Identity Card number', '{{subject}} must not be a Polish Identity Card number', )] +#[Assurance(type: 'string')] final class PolishIdCard extends Simple { private const int ASCII_CODE_0 = 48; diff --git a/src/Validators/PortugueseNif.php b/src/Validators/PortugueseNif.php index c163d8d0a..9b4fb7aca 100644 --- a/src/Validators/PortugueseNif.php +++ b/src/Validators/PortugueseNif.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -32,6 +33,7 @@ '{{subject}} must be a Portuguese NIF', '{{subject}} must not be a Portuguese NIF', )] +#[Assurance(type: 'string')] final class PortugueseNif extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Positive.php b/src/Validators/Positive.php index 748180d63..9607a69b1 100644 --- a/src/Validators/Positive.php +++ b/src/Validators/Positive.php @@ -15,18 +15,20 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +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', '{{subject}} must not be a positive number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Positive extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/PostalCode.php b/src/Validators/PostalCode.php index 9c74df8ab..508982b25 100644 --- a/src/Validators/PostalCode.php +++ b/src/Validators/PostalCode.php @@ -26,6 +26,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; @@ -37,6 +38,7 @@ '{{subject}} must be a postal code for {{countryCode}}', '{{subject}} must not be a postal code for {{countryCode}}', )] +#[Assurance(type: 'string')] final class PostalCode extends Envelope { private const array POSTAL_CODES_EXTRA = [ diff --git a/src/Validators/Printable.php b/src/Validators/Printable.php index df6fa7a7b..60ba77d24 100644 --- a/src/Validators/Printable.php +++ b/src/Validators/Printable.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of printable characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Printable extends FilteredString { protected function isValid(string $input): bool 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/PublicDomainSuffix.php b/src/Validators/PublicDomainSuffix.php index 002f88d86..b82b073ee 100644 --- a/src/Validators/PublicDomainSuffix.php +++ b/src/Validators/PublicDomainSuffix.php @@ -12,6 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -36,6 +37,7 @@ '{{subject}} must be a public domain suffix', '{{subject}} must not be a public domain suffix', )] +#[Assurance(type: 'string')] final class PublicDomainSuffix extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Punct.php b/src/Validators/Punct.php index b781f299d..a1e81face 100644 --- a/src/Validators/Punct.php +++ b/src/Validators/Punct.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of punctuation characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Punct extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Readable.php b/src/Validators/Readable.php index 641e50812..2424ecb31 100644 --- a/src/Validators/Readable.php +++ b/src/Validators/Readable.php @@ -17,6 +17,7 @@ use Attribute; use Psr\Http\Message\StreamInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -29,6 +30,7 @@ '{{subject}} must be readable', '{{subject}} must not be readable', )] +#[Assurance(type: ['string', SplFileInfo::class, StreamInterface::class])] final class Readable extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Regex.php b/src/Validators/Regex.php index 679d277b3..562f90b60 100644 --- a/src/Validators/Regex.php +++ b/src/Validators/Regex.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -28,6 +29,7 @@ '{{subject}} must match the {{regex|quote}} pattern', '{{subject}} must not match the {{regex|quote}} pattern', )] +#[Assurance(type: 'string')] final readonly class Regex implements Validator { public function __construct( diff --git a/src/Validators/ResourceType.php b/src/Validators/ResourceType.php index 5fcd6a767..d78fa50a9 100644 --- a/src/Validators/ResourceType.php +++ b/src/Validators/ResourceType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be an internal resource', '{{subject}} must not be an internal resource', )] +#[Assurance(type: 'resource')] final class ResourceType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Roman.php b/src/Validators/Roman.php index d1c7959f2..73ea46b1a 100644 --- a/src/Validators/Roman.php +++ b/src/Validators/Roman.php @@ -18,6 +18,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; @@ -26,6 +27,7 @@ '{{subject}} must be a Roman numeral', '{{subject}} must not be a Roman numeral', )] +#[Assurance(type: 'string')] final class Roman extends Envelope { public function __construct() diff --git a/src/Validators/ScalarVal.php b/src/Validators/ScalarVal.php index 2edbcefd1..e7f2fc9d7 100644 --- a/src/Validators/ScalarVal.php +++ b/src/Validators/ScalarVal.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be a scalar', '{{subject}} must not be a scalar', )] +#[Assurance(type: 'int|float|bool|string')] final class ScalarVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Slug.php b/src/Validators/Slug.php index dfd9f2b7e..f849f965a 100644 --- a/src/Validators/Slug.php +++ b/src/Validators/Slug.php @@ -19,6 +19,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be a slug', '{{subject}} must not be a slug', )] +#[Assurance(type: 'string')] final class Slug extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Sorted.php b/src/Validators/Sorted.php index b8debd798..222783c71 100644 --- a/src/Validators/Sorted.php +++ b/src/Validators/Sorted.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -35,6 +36,7 @@ '{{subject}} must not be sorted in descending order', self::TEMPLATE_DESCENDING, )] +#[Assurance(type: 'array|string')] final readonly class Sorted implements Validator { public const string TEMPLATE_ASCENDING = '__ascending__'; diff --git a/src/Validators/Space.php b/src/Validators/Space.php index b39cb6503..96752df7b 100644 --- a/src/Validators/Space.php +++ b/src/Validators/Space.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of space characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Space extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Spaced.php b/src/Validators/Spaced.php index b8c020d4c..a407b2a76 100644 --- a/src/Validators/Spaced.php +++ b/src/Validators/Spaced.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must contain at least one whitespace', '{{subject}} must not contain whitespace', )] +#[Assurance(type: 'string')] final class Spaced extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/StringType.php b/src/Validators/StringType.php index 50bc93356..e9d456c2a 100644 --- a/src/Validators/StringType.php +++ b/src/Validators/StringType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be a string', '{{subject}} must not be a string', )] +#[Assurance(type: 'string')] final class StringType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/StringVal.php b/src/Validators/StringVal.php index 9126ce561..4c2270a02 100644 --- a/src/Validators/StringVal.php +++ b/src/Validators/StringVal.php @@ -17,8 +17,10 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; +use Stringable; use function is_object; use function is_scalar; @@ -29,6 +31,7 @@ '{{subject}} must be a string', '{{subject}} must not be a string', )] +#[Assurance(type: ['scalar', Stringable::class])] final class StringVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/SubdivisionCode.php b/src/Validators/SubdivisionCode.php index da3c721f1..21fb99083 100644 --- a/src/Validators/SubdivisionCode.php +++ b/src/Validators/SubdivisionCode.php @@ -13,6 +13,7 @@ use Attribute; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -28,6 +29,7 @@ '{{subject}} must be a subdivision code of {{countryName|trans}}', '{{subject}} must not be a subdivision code of {{countryName|trans}}', )] +#[Assurance(type: 'string')] final readonly class SubdivisionCode implements Validator { use CanValidateUndefined; diff --git a/src/Validators/Subset.php b/src/Validators/Subset.php index dbde4cdf0..1897663b8 100644 --- a/src/Validators/Subset.php +++ b/src/Validators/Subset.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -29,6 +30,7 @@ '{{subject}} must be subset of {{superset}}', '{{subject}} must not be subset of {{superset}}', )] +#[Assurance(type: 'array')] final readonly class Subset implements Validator { /** @param mixed[] $superset */ diff --git a/src/Validators/SymbolicLink.php b/src/Validators/SymbolicLink.php index fc7513260..6b8aa619e 100644 --- a/src/Validators/SymbolicLink.php +++ b/src/Validators/SymbolicLink.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -27,6 +28,7 @@ '{{subject}} must be an accessible existing symbolic link', '{{subject}} must not be an accessible existing symbolic link', )] +#[Assurance(type: ['string', SplFileInfo::class])] final class SymbolicLink extends Simple { public function isValid(mixed $input): bool 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/Time.php b/src/Validators/Time.php index 297c4439a..47799a85c 100644 --- a/src/Validators/Time.php +++ b/src/Validators/Time.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; @@ -33,6 +34,7 @@ '{{subject}} must be a time in the {{sample}} format', '{{subject}} must not be a time in the {{sample}} format', )] +#[Assurance(type: 'string')] final readonly class Time implements Validator { use CanValidateDateTime; diff --git a/src/Validators/Tld.php b/src/Validators/Tld.php index 9221bed8a..af9f52a94 100644 --- a/src/Validators/Tld.php +++ b/src/Validators/Tld.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; @@ -29,6 +30,7 @@ '{{subject}} must be a top-level domain name', '{{subject}} must not be a top-level domain name', )] +#[Assurance(type: 'string')] final class Tld extends Envelope { public function __construct() diff --git a/src/Validators/Trimmed.php b/src/Validators/Trimmed.php index 88588aa1b..d3ef59c5a 100644 --- a/src/Validators/Trimmed.php +++ b/src/Validators/Trimmed.php @@ -11,6 +11,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\Envelope; @@ -26,6 +27,7 @@ '{{subject}} must contain leading or trailing {{trimValues|list:or}}', self::TEMPLATE_CUSTOM, )] +#[Assurance(type: 'string')] final class Trimmed extends Envelope { public const string TEMPLATE_CUSTOM = '__custom__'; diff --git a/src/Validators/TrueVal.php b/src/Validators/TrueVal.php index 7d99308f7..2c21fffcc 100644 --- a/src/Validators/TrueVal.php +++ b/src/Validators/TrueVal.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,6 +27,7 @@ '{{subject}} must evaluate to `true`', '{{subject}} must not evaluate to `true`', )] +#[Assurance(type: 'true')] final class TrueVal extends Simple { public function isValid(mixed $input): bool 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..fcffbef11 100644 --- a/src/Validators/UndefOr.php +++ b/src/Validators/UndefOr.php @@ -13,7 +13,9 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceModifier; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -21,12 +23,16 @@ 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', 'and must not be undefined', )] +#[Assurance(modifier: AssuranceModifier::Nullable)] final readonly class UndefOr implements Validator { use CanValidateUndefined; diff --git a/src/Validators/Unique.php b/src/Validators/Unique.php index 724539bb5..c31fb7686 100644 --- a/src/Validators/Unique.php +++ b/src/Validators/Unique.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must not contain duplicates', '{{subject}} must contain duplicates', )] +#[Assurance(type: 'array')] final class Unique extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Uppercase.php b/src/Validators/Uppercase.php index 5ae7785f0..42bb6a418 100644 --- a/src/Validators/Uppercase.php +++ b/src/Validators/Uppercase.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must consist only of uppercase letters', '{{subject}} must not consist only of uppercase letters', )] +#[Assurance(type: 'string')] final class Uppercase extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Url.php b/src/Validators/Url.php index 92600a2ab..456d1cdbb 100644 --- a/src/Validators/Url.php +++ b/src/Validators/Url.php @@ -12,6 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -26,6 +27,7 @@ '{{subject}} must be a URL', '{{subject}} must not be a URL', )] +#[Assurance(type: 'string')] final class Url implements Validator { private readonly Validator $validator; diff --git a/src/Validators/Uuid.php b/src/Validators/Uuid.php index 654df0a0c..c7d59bb67 100644 --- a/src/Validators/Uuid.php +++ b/src/Validators/Uuid.php @@ -22,6 +22,7 @@ use Ramsey\Uuid\Rfc4122\FieldsInterface; use Ramsey\Uuid\UuidFactory; use Ramsey\Uuid\UuidInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -43,6 +44,7 @@ '{{subject}} must not be a UUID v{{version|raw}}', self::TEMPLATE_VERSION, )] +#[Assurance(type: 'string')] final class Uuid implements Validator { public const string TEMPLATE_VERSION = '__version__'; diff --git a/src/Validators/Version.php b/src/Validators/Version.php index 62d311db6..7b2343a6a 100644 --- a/src/Validators/Version.php +++ b/src/Validators/Version.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be a version number', '{{subject}} must not be a version number', )] +#[Assurance(type: 'string')] final class Version extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Vowel.php b/src/Validators/Vowel.php index 699e9695e..e5e67b66f 100644 --- a/src/Validators/Vowel.php +++ b/src/Validators/Vowel.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -33,6 +34,7 @@ '{{subject}} must not consist of vowels or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Vowel extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/When.php b/src/Validators/When.php index 75cbcd7ef..8f7fa226f 100644 --- a/src/Validators/When.php +++ b/src/Validators/When.php @@ -17,10 +17,13 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; use Respect\Validation\Result; use Respect\Validation\Validator; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +#[Assurance(compose: AssuranceCompose::Union, composeRange: [1, null])] final readonly class When implements Validator { public function __construct( diff --git a/src/Validators/Writable.php b/src/Validators/Writable.php index 6f33716a0..221c3d0f2 100644 --- a/src/Validators/Writable.php +++ b/src/Validators/Writable.php @@ -17,6 +17,7 @@ use Attribute; use Psr\Http\Message\StreamInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -29,6 +30,7 @@ '{{subject}} must be an accessible existing writable filesystem entry', '{{subject}} must not be an accessible existing writable filesystem entry', )] +#[Assurance(type: ['string', SplFileInfo::class, StreamInterface::class])] final class Writable extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Xdigit.php b/src/Validators/Xdigit.php index 4544c0632..1a654ab73 100644 --- a/src/Validators/Xdigit.php +++ b/src/Validators/Xdigit.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -31,6 +32,7 @@ '{{subject}} must not consist only of hexadecimal digits or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Xdigit extends FilteredString { protected function isValid(string $input): bool diff --git a/tests/benchmark/PrefixBench.php b/tests/benchmark/PrefixBench.php deleted file mode 100644 index 233660fc4..000000000 --- a/tests/benchmark/PrefixBench.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Validation\Benchmarks; - -use PhpBench\Attributes as Bench; -use Respect\Validation\Transformers\Prefix; -use Respect\Validation\Transformers\ValidatorSpec; - -final class PrefixBench -{ - /** @param array{0: Prefix, 1: ValidatorSpec} $params */ - #[Bench\ParamProviders(['provideTransformerSpec'])] - #[Bench\Iterations(10)] - #[Bench\RetryThreshold(5)] - #[Bench\Revs(100)] - #[Bench\Warmup(1)] - #[Bench\Subject] - public function prefixTransformer(array $params): void - { - $params[0]->transform($params[1]); - } - - /** @return array */ - public static function provideTransformerSpec(): array - { - return [ - [new Prefix(), new ValidatorSpec('keyName', ['value', 'other'])], - [new Prefix(), new ValidatorSpec('propertyTitle', ['value', 'other'])], - [new Prefix(), new ValidatorSpec('notSomething', ['value'])], - [new Prefix(), new ValidatorSpec('not')], - [new Prefix(), new ValidatorSpec('arrayVal')], - ]; - } -} diff --git a/tests/inference/NarrowingTest.php b/tests/inference/NarrowingTest.php new file mode 100644 index 000000000..3fcfb1aa2 --- /dev/null +++ b/tests/inference/NarrowingTest.php @@ -0,0 +1,39 @@ + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../vendor/respect/fluent-analysis/extension.neon', + __DIR__ . '/../../fluent.neon', + ]; + } + + /** @return iterable */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/assertions/narrowing.php'); + } + + #[Test] + #[DataProvider('dataFileAsserts')] + public function fileAsserts(string $assertType, string $file, mixed ...$args): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } +} diff --git a/tests/inference/assertions/narrowing.php b/tests/inference/assertions/narrowing.php new file mode 100644 index 000000000..df2d62468 --- /dev/null +++ b/tests/inference/assertions/narrowing.php @@ -0,0 +1,475 @@ +assert($x); + assertType('int', $x); +} + +function stringTypeAssert(mixed $x): void +{ + v::stringType()->assert($x); + assertType('string', $x); +} + +function floatTypeAssert(mixed $x): void +{ + v::floatType()->assert($x); + assertType('float', $x); +} + +function boolTypeAssert(mixed $x): void +{ + v::boolType()->assert($x); + assertType('bool', $x); +} + +function nullTypeAssert(mixed $x): void +{ + v::nullType()->assert($x); + assertType('null', $x); +} + +function arrayTypeAssert(mixed $x): void +{ + v::arrayType()->assert($x); + assertType('array', $x); +} + +function objectTypeAssert(mixed $x): void +{ + v::objectType()->assert($x); + assertType('object', $x); +} + +// --- Instance (dynamic) --- + +function instanceAssert(mixed $x): void +{ + v::instance(DateTimeInterface::class)->assert($x); + assertType('DateTimeInterface', $x); +} + +// --- Val validators --- + +function intValAssert(mixed $x): void +{ + v::intVal()->assert($x); + assertType('int|numeric-string', $x); +} + +function numericValAssert(mixed $x): void +{ + v::numericVal()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function scalarValAssert(mixed $x): void +{ + v::scalarVal()->assert($x); + assertType('bool|float|int|string', $x); +} + +// --- Composable prefixes --- + +function nullOrIntTypeAssert(mixed $x): void +{ + v::nullOrIntType()->assert($x); + assertType('int|null', $x); +} + +function notIntTypeAssert(int|string $x): void +{ + v::notIntType()->assert($x); + assertType('string', $x); +} + +// --- Chain intersection --- + +function chainIntersection(mixed $x): void +{ + v::intType()->positive()->assert($x); + assertType('int', $x); +} + +// --- check() works too --- + +function checkNarrowing(mixed $x): void +{ + v::stringType()->check($x); + assertType('string', $x); +} + +// --- isValid() type guard --- + +function isValidGuard(mixed $x): void +{ + if (!v::intType()->isValid($x)) { + return; + } + + assertType('int', $x); +} + +function isValidFalsey(int|string $x): void +{ + if (v::intType()->isValid($x)) { + return; + } + + assertType('string', $x); +} + +// --- P0: String format validators --- + +function emailAssert(mixed $x): void +{ + v::email()->assert($x); + assertType('string', $x); +} + +function uuidAssert(mixed $x): void +{ + v::uuid()->assert($x); + assertType('string', $x); +} + +function urlAssert(mixed $x): void +{ + v::url()->assert($x); + assertType('string', $x); +} + +function jsonAssert(mixed $x): void +{ + v::json()->assert($x); + assertType('string', $x); +} + +function alphaAssert(mixed $x): void +{ + v::alpha()->assert($x); + assertType('string', $x); +} + +function digitAssert(mixed $x): void +{ + v::digit()->assert($x); + assertType('string', $x); +} + +// --- P0: Filesystem validators --- + +function fileAssert(mixed $x): void +{ + v::file()->assert($x); + assertType('SplFileInfo|string', $x); +} + +function directoryAssert(mixed $x): void +{ + v::directory()->assert($x); + assertType('SplFileInfo|string', $x); +} + +// --- P0: Array validators --- + +function sortedAssert(mixed $x): void +{ + v::sorted('ASC')->assert($x); + assertType('array|string', $x); +} + +function uniqueAssert(mixed $x): void +{ + v::unique()->assert($x); + assertType('array', $x); +} + +// --- P0: Numeric validators --- + +function multipleAssert(mixed $x): void +{ + v::multiple(3)->assert($x); + assertType('int', $x); +} + +// --- P0: DateTime --- + +function dateTimeAssert(mixed $x): void +{ + v::dateTime()->assert($x); + assertType('DateTimeInterface|string', $x); +} + +// --- P0: Composable prefix + new narrowing --- + +function nullOrEmailAssert(mixed $x): void +{ + v::nullOrEmail()->assert($x); + assertType('string|null', $x); +} + +// --- P0: Chain with string format --- + +function stringTypeEmailChain(mixed $x): void +{ + v::stringType()->email()->assert($x); + assertType('string', $x); +} + +// --- Identical (value mode) --- + +function identicalIntAssert(mixed $x): void +{ + v::identical(42)->assert($x); + assertType('42', $x); +} + +function identicalStringAssert(mixed $x): void +{ + v::identical('foo')->assert($x); + assertType("'foo'", $x); +} + +// --- In (member mode) --- + +function inArrayAssert(mixed $x): void +{ + v::in(['active', 'inactive', 'pending'])->assert($x); + assertType("'active'|'inactive'|'pending'", $x); +} + +function inIntArrayAssert(mixed $x): void +{ + v::in([1, 2, 3])->assert($x); + assertType('1|2|3', $x); +} + +// --- AnyOf (children union) --- + +function anyOfAssert(mixed $x): void +{ + v::anyOf(v::intType(), v::stringType())->assert($x); + assertType('int|string', $x); +} + +// --- AllOf (children intersect) --- + +function allOfAssert(mixed $x): void +{ + v::allOf(v::intType(), v::positive())->assert($x); + assertType('int', $x); +} + +// --- Each (elements) --- + +function eachAssert(mixed $x): void +{ + v::each(v::intType())->assert($x); + assertType('array', $x); +} + +function eachStringAssert(mixed $x): void +{ + v::each(v::email())->assert($x); + assertType('array', $x); +} + +// --- When (childrenRange) --- + +function whenAssert(mixed $x): void +{ + v::when(v::intType(), v::intType(), v::stringType())->assert($x); + assertType('int|string', $x); +} + +// --- NoneOf (children union + remove) --- + +function noneOfAssert(int|string|float $x): void +{ + v::noneOf(v::intType(), v::stringType())->assert($x); + assertType('float', $x); +} + +// --- Additional type validators --- + +function callableTypeAssert(mixed $x): void +{ + v::callableType()->assert($x); + assertType('callable(): mixed', $x); +} + +function resourceTypeAssert(mixed $x): void +{ + v::resourceType()->assert($x); + assertType('resource', $x); +} + +function iterableTypeAssert(mixed $x): void +{ + v::iterableType()->assert($x); + assertType('iterable', $x); +} + +// --- Bool variants --- + +function trueValAssert(mixed $x): void +{ + v::trueVal()->assert($x); + assertType('true', $x); +} + +function falseValAssert(mixed $x): void +{ + v::falseVal()->assert($x); + assertType('false', $x); +} + +function boolValAssert(mixed $x): void +{ + v::boolVal()->assert($x); + assertType('bool', $x); +} + +// --- Val variants --- + +function floatValAssert(mixed $x): void +{ + v::floatVal()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function stringValAssert(mixed $x): void +{ + v::stringVal()->assert($x); + assertType('bool|float|int|string|Stringable', $x); +} + +function arrayValAssert(mixed $x): void +{ + v::arrayVal()->assert($x); + assertType('array|ArrayAccess', $x); +} + +function iterableValAssert(mixed $x): void +{ + v::iterableVal()->assert($x); + assertType('array|stdClass|Traversable', $x); +} + +// --- Collection --- + +function countableAssert(mixed $x): void +{ + v::countable()->assert($x); + assertType('array|Countable', $x); +} + +// --- Numeric --- + +function positiveAssert(mixed $x): void +{ + v::positive()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function negativeAssert(mixed $x): void +{ + v::negative()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function evenAssert(mixed $x): void +{ + v::even()->assert($x); + assertType('int', $x); +} + +function oddAssert(mixed $x): void +{ + v::odd()->assert($x); + assertType('int', $x); +} + +function factorAssert(mixed $x): void +{ + v::factor(10)->assert($x); + assertType('int', $x); +} + +function finiteAssert(mixed $x): void +{ + v::finite()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function infiniteAssert(mixed $x): void +{ + v::infinite()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function numberAssert(mixed $x): void +{ + v::number()->assert($x); + assertType('float|int|numeric-string', $x); +} + +// --- Date/Time --- + +function dateAssert(mixed $x): void +{ + v::date()->assert($x); + assertType('string', $x); +} + +function timeAssert(mixed $x): void +{ + v::time()->assert($x); + assertType('string', $x); +} + +function leapYearAssert(mixed $x): void +{ + v::leapYear()->assert($x); + assertType('int|string', $x); +} + +// --- Composites --- + +function oneOfAssert(mixed $x): void +{ + v::oneOf(v::intType(), v::stringType())->assert($x); + assertType('int|string', $x); +} + +function allAssert(mixed $x): void +{ + v::all(v::intType())->assert($x); + assertType('array', $x); +} + +// --- Modifier: undefOr --- + +function undefOrIntTypeAssert(mixed $x): void +{ + v::undefOrIntType()->assert($x); + assertType('int|null', $x); +} diff --git a/tests/src/Transformers/StubTransformer.php b/tests/src/Transformers/StubTransformer.php deleted file mode 100644 index caea9ec3c..000000000 --- a/tests/src/Transformers/StubTransformer.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Test\Transformers; - -use Respect\Validation\Transformers\Transformer; -use Respect\Validation\Transformers\ValidatorSpec; - -final class StubTransformer implements Transformer -{ - public function transform(ValidatorSpec $validatorSpec): ValidatorSpec - { - return $validatorSpec; - } -} 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/ContainerRegistryTest.php b/tests/unit/ContainerRegistryTest.php index f0d8c6674..1dd9feac3 100644 --- a/tests/unit/ContainerRegistryTest.php +++ b/tests/unit/ContainerRegistryTest.php @@ -50,4 +50,21 @@ public function itAllowsOverwritingTheContainer(): void self::assertSame($newContainer, ContainerRegistry::getContainer()); } + + #[Test] + public function extraNamespacesResolveCustomValidators(): void + { + $mainContainer = ContainerRegistry::getContainer(); + ContainerRegistry::setContainer(ContainerRegistry::createContainer([ + 'respect.validation.rule_factory.namespaces' => ['Respect\\Validation\\Test\\Validators'], + ])); + + try { + // 'CustomRule' exists in Test\Validators but not in Validators + $builder = ValidatorBuilder::customRule(); // @phpstan-ignore staticMethod.notFound + self::assertCount(1, $builder->getValidators()); + } finally { + ContainerRegistry::setContainer($mainContainer); + } + } } 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/Transformers/PrefixTest.php b/tests/unit/Transformers/PrefixTest.php deleted file mode 100644 index 9fedb4d58..000000000 --- a/tests/unit/Transformers/PrefixTest.php +++ /dev/null @@ -1,102 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Transformers; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\Test\TestCase; - -#[Group('core')] -#[CoversClass(Prefix::class)] -final class PrefixTest extends TestCase -{ - #[Test] - #[DataProvider('providerForTransformedValidatorSpec')] - public function itShouldTransformValidatorSpec(ValidatorSpec $original, ValidatorSpec $expected): void - { - $transformer = new Prefix(); - $transformed = $transformer->transform($original); - - self::assertEquals($expected, $transformed); - } - - #[Test] - #[DataProvider('providerForUntransformedRuleNames')] - public function itShouldPreventTransformingCanonicalRule(string $ruleName): void - { - $validatorSpec = new ValidatorSpec($ruleName); - - $transformer = new Prefix(); - self::assertSame($validatorSpec, $transformer->transform($validatorSpec)); - } - - /** @return array */ - public static function providerForTransformedValidatorSpec(): array - { - return [ - 'key' => [ - new ValidatorSpec('keyNextRule', ['keyName', 123]), - new ValidatorSpec('NextRule', [123], new ValidatorSpec('key', ['keyName'])), - ], - 'length' => [ - new ValidatorSpec('lengthNextRule', [5]), - new ValidatorSpec('NextRule', [5], new ValidatorSpec('length')), - ], - 'max' => [ - new ValidatorSpec('maxNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('max')), - ], - 'min' => [ - new ValidatorSpec('minNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('min')), - ], - 'not' => [ - new ValidatorSpec('notNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('not')), - ], - 'nullOr' => [ - new ValidatorSpec('nullOrNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('nullOr')), - ], - 'property' => [ - new ValidatorSpec('propertyNextRule', ['propertyName', 567]), - new ValidatorSpec('NextRule', [567], new ValidatorSpec('property', ['propertyName'])), - ], - 'undefOr' => [ - new ValidatorSpec('undefOrNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('undefOr')), - ], - ]; - } - - /** @return array */ - public static function providerForUntransformedRuleNames(): array - { - return [ - 'equals' => ['equals'], - 'key' => ['key'], - 'keyExists' => ['keyExists'], - 'keyOptional' => ['keyOptional'], - 'keySet' => ['keySet'], - 'length' => ['length'], - 'max' => ['max'], - 'min' => ['min'], - 'not' => ['not'], - 'undef' => ['undef'], - 'property' => ['property'], - 'propertyExists' => ['propertyExists'], - 'propertyOptional' => ['propertyOptional'], - ]; - } -} diff --git a/tests/unit/ValidatorBuilderTest.php b/tests/unit/ValidatorBuilderTest.php index 3f152a573..d17642362 100644 --- a/tests/unit/ValidatorBuilderTest.php +++ b/tests/unit/ValidatorBuilderTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\Attributes\Test; +use Respect\Fluent\Exceptions\CouldNotResolve; use Respect\Validation\Exceptions\ComponentException; use Respect\Validation\Exceptions\ValidationException; use Respect\Validation\Test\TestCase; @@ -31,7 +32,7 @@ final class ValidatorBuilderTest extends TestCase #[Test] public function invalidRuleClassShouldThrowComponentException(): void { - $this->expectException(ComponentException::class); + $this->expectException(CouldNotResolve::class); // @phpstan-ignore-next-line ValidatorBuilder::iDoNotExistSoIShouldThrowException(); @@ -42,7 +43,6 @@ public function shouldReturnValidatorInstanceWhenTheNotRuleIsCalledWithArguments { $validator = ValidatorBuilder::init(); - // @phpstan-ignore-next-line self::assertNotSame($validator, $validator->not($validator->falsy())); }