diff --git a/composer.json b/composer.json index 68ac3fdff..96b189202 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "php": ">=8.5", "php-di/php-di": "^7.1", "psr/container": "^2.0", + "respect/fluent": "^2.0", "respect/string-formatter": "^1.7", "respect/stringifier": "^3.0", "symfony/polyfill-intl-idn": "^1.33", @@ -43,6 +44,7 @@ "psr/http-message": "^1.0 || ^2.0", "ramsey/uuid": "^4", "respect/coding-standard": "^5.0", + "respect/fluentgen": "^2.0", "sebastian/diff": "^7.0", "sokil/php-isocodes": "^4.2.1", "sokil/php-isocodes-db-only": "^4.0", diff --git a/composer.lock b/composer.lock index 5b50b02e6..04f386930 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": "6e033061e6abc0e6499d8cc89427a150", + "content-hash": "c007f19e671e2fec2eb7341be4146b75", "packages": [ { "name": "laravel/serializable-closure", - "version": "v2.0.12", + "version": "v2.0.13", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919" + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/a6abb4e54f6fcd3138120b9ad497f0bd146f9919", - "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", "shasum": "" }, "require": { @@ -65,7 +65,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-04-14T13:33:34+00:00" + "time": "2026-04-16T14:03:50+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", @@ -308,16 +361,16 @@ }, { "name": "respect/stringifier", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/Respect/Stringifier.git", - "reference": "291b6248c93787cf3b6c2be59c98e420c2abd5d6" + "reference": "7e3d7d665f104ea5d1acad76355ea2be6a0dd6b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Respect/Stringifier/zipball/291b6248c93787cf3b6c2be59c98e420c2abd5d6", - "reference": "291b6248c93787cf3b6c2be59c98e420c2abd5d6", + "url": "https://api.github.com/repos/Respect/Stringifier/zipball/7e3d7d665f104ea5d1acad76355ea2be6a0dd6b3", + "reference": "7e3d7d665f104ea5d1acad76355ea2be6a0dd6b3", "shasum": "" }, "require": { @@ -359,9 +412,9 @@ ], "support": { "issues": "https://github.com/Respect/Stringifier/issues", - "source": "https://github.com/Respect/Stringifier/tree/3.0.0" + "source": "https://github.com/Respect/Stringifier/tree/3.1.0" }, - "time": "2026-01-19T10:24:52+00:00" + "time": "2026-05-10T19:20:44+00:00" }, { "name": "symfony/polyfill-intl-idn", @@ -859,28 +912,29 @@ }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -918,7 +972,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -928,13 +982,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/xdebug-handler", @@ -1004,16 +1054,16 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", "shasum": "" }, "require": { @@ -1096,7 +1146,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-11T04:32:07+00:00" + "time": "2026-05-06T08:26:05+00:00" }, { "name": "doctrine/annotations", @@ -1881,16 +1931,16 @@ }, { "name": "nette/utils", - "version": "v4.1.3", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", - "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", "shasum": "" }, "require": { @@ -1966,9 +2016,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.3" + "source": "https://github.com/nette/utils/tree/v4.1.4" }, - "time": "2026-02-13T03:05:33+00:00" + "time": "2026-05-11T20:49:54+00:00" }, { "name": "nikic/php-parser", @@ -4146,6 +4196,64 @@ }, "time": "2026-01-19T10:34:07+00:00" }, + { + "name": "respect/fluentgen", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/Respect/FluentGen.git", + "reference": "6a9065516f403c5f5abc86646290bd08e44c538e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/FluentGen/zipball/6a9065516f403c5f5abc86646290bd08e44c538e", + "reference": "6a9065516f403c5f5abc86646290bd08e44c538e", + "shasum": "" + }, + "require": { + "nette/php-generator": "^4.1", + "php": "^8.5" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^12.5", + "respect/coding-standard": "^5.0", + "respect/fluent": "^2.0" + }, + "suggest": { + "respect/fluent": "Enables #[Composable] prefix composition support" + }, + "type": "library", + "autoload": { + "psr-4": { + "Respect\\FluentGen\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Respect/FluentGen Contributors", + "homepage": "https://github.com/Respect/FluentGen/graphs/contributors" + } + ], + "description": "Code generation for fluent builder interfaces", + "keywords": [ + "fluent", + "fluentgen", + "mixin", + "respect" + ], + "support": { + "issues": "https://github.com/Respect/FluentGen/issues", + "source": "https://github.com/Respect/FluentGen/tree/2.0.0" + }, + "time": "2026-03-25T05:50:09+00:00" + }, { "name": "sebastian/cli-parser", "version": "4.2.1", @@ -5057,16 +5165,16 @@ }, { "name": "seld/jsonlint", - "version": "1.11.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + "reference": "d95c42df9c4a713ca1d4a45b37f0416dee1ea73a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d95c42df9c4a713ca1d4a45b37f0416dee1ea73a", + "reference": "d95c42df9c4a713ca1d4a45b37f0416dee1ea73a", "shasum": "" }, "require": { @@ -5105,7 +5213,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.0" }, "funding": [ { @@ -5117,24 +5225,24 @@ "type": "tidelift" } ], - "time": "2024-07-11T14:55:45+00:00" + "time": "2026-06-11T13:43:55+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.28.1", + "version": "8.29.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "66151cfbd25b50e8becd9f809fb704f01fd4d6f2" + "reference": "81fce13c4ef4b53a03e5cfa6ce36afc191c1598e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/66151cfbd25b50e8becd9f809fb704f01fd4d6f2", - "reference": "66151cfbd25b50e8becd9f809fb704f01fd4d6f2", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/81fce13c4ef4b53a03e5cfa6ce36afc191c1598e", + "reference": "81fce13c4ef4b53a03e5cfa6ce36afc191c1598e", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.2.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.2.1", "php": "^7.4 || ^8.0", "phpstan/phpdoc-parser": "^2.3.2", "squizlabs/php_codesniffer": "^4.0.1" @@ -5142,11 +5250,11 @@ "require-dev": { "phing/phing": "3.0.1|3.1.2", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.42", + "phpstan/phpstan": "2.1.54", "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" + "phpstan/phpstan-strict-rules": "2.0.11", + "phpunit/phpunit": "9.6.34|10.5.63|11.4.4|11.5.55|12.5.24" }, "type": "phpcodesniffer-standard", "extra": { @@ -5170,7 +5278,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.28.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.29.0" }, "funding": [ { @@ -5182,7 +5290,7 @@ "type": "tidelift" } ], - "time": "2026-03-22T17:22:38+00:00" + "time": "2026-05-07T05:48:08+00:00" }, { "name": "sokil/php-isocodes", @@ -5882,16 +5990,16 @@ }, { "name": "symfony/polyfill-deepclone", - "version": "v1.37.0", + "version": "v1.39.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-deepclone.git", - "reference": "2ca9e9e75ead5174f2b44613a646bdc9338b8eb4" + "reference": "1b034bc050d84cc9c187de373f744912e1e35f1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/2ca9e9e75ead5174f2b44613a646bdc9338b8eb4", - "reference": "2ca9e9e75ead5174f2b44613a646bdc9338b8eb4", + "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/1b034bc050d84cc9c187de373f744912e1e35f1f", + "reference": "1b034bc050d84cc9c187de373f744912e1e35f1f", "shasum": "" }, "require": { @@ -5945,7 +6053,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.39.0" }, "funding": [ { @@ -5965,7 +6073,7 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:03:27+00:00" + "time": "2026-06-10T20:07:50+00:00" }, { "name": "symfony/polyfill-intl-grapheme", diff --git a/src-dev/CodeGen/Config.php b/src-dev/CodeGen/Config.php deleted file mode 100644 index af3a8864f..000000000 --- a/src-dev/CodeGen/Config.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -final readonly class Config -{ - public function __construct( - public string $sourceDir, - public string $sourceNamespace, - public string $outputDir, - public string $outputNamespace, - public OutputFormatter $outputFormatter = new OutputFormatter(), - ) { - } -} diff --git a/src-dev/CodeGen/FluentBuilder/MethodBuilder.php b/src-dev/CodeGen/FluentBuilder/MethodBuilder.php deleted file mode 100644 index 72da85787..000000000 --- a/src-dev/CodeGen/FluentBuilder/MethodBuilder.php +++ /dev/null @@ -1,169 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\Method; -use Nette\PhpGenerator\PhpNamespace; -use ReflectionClass; -use ReflectionNamedType; -use ReflectionParameter; -use ReflectionUnionType; - -use function count; -use function implode; -use function in_array; -use function is_object; -use function lcfirst; -use function preg_replace; -use function sort; -use function str_starts_with; -use function ucfirst; - -final class MethodBuilder -{ - /** - * @param array $excludedTypePrefixes - * @param array $excludedTypeNames - */ - public function __construct( - private readonly array $excludedTypePrefixes = [], - private readonly array $excludedTypeNames = [], - ) { - } - - public function build( - PhpNamespace $namespace, - ReflectionClass $nodeReflection, - string $returnType, - string|null $prefix = null, - bool $static = false, - ReflectionParameter|null $prefixParameter = null, - ): Method { - $originalName = $nodeReflection->getShortName(); - $name = $prefix ? $prefix . ucfirst($originalName) : lcfirst($originalName); - - $method = new Method($name); - $method->setPublic()->setReturnType($returnType); - - if ($static) { - $method->setStatic(); - } - - if ($prefixParameter !== null) { - $this->addPrefixParameter($method, $prefixParameter); - } - - $constructor = $nodeReflection->getConstructor(); - if ($constructor === null) { - return $method; - } - - $comment = $constructor->getDocComment(); - if ($comment) { - $method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $comment)); - } - - foreach ($constructor->getParameters() as $reflectionParameter) { - $this->addParameter($method, $reflectionParameter, $namespace); - } - - return $method; - } - - private function addPrefixParameter(Method $method, ReflectionParameter $reflectionParameter): void - { - $type = $reflectionParameter->getType(); - $types = []; - - if ($type instanceof ReflectionUnionType) { - foreach ($type->getTypes() as $subType) { - $types[] = $subType->getName(); - } - - sort($types); - } elseif ($type instanceof ReflectionNamedType) { - $types[] = $type->getName(); - } - - $method->addParameter($reflectionParameter->getName())->setType(implode('|', $types)); - } - - private function addParameter( - Method $method, - ReflectionParameter $reflectionParameter, - PhpNamespace $namespace, - ): void { - if ($reflectionParameter->isVariadic()) { - $method->setVariadic(); - } - - $type = $reflectionParameter->getType(); - $types = []; - - if ($type instanceof ReflectionUnionType) { - foreach ($type->getTypes() as $subType) { - $types[] = $subType->getName(); - if ($subType->isBuiltin()) { - continue; - } - - $namespace->addUse($subType->getName()); - } - } elseif ($type instanceof ReflectionNamedType) { - $types[] = $type->getName(); - - if ($this->isExcludedType($type->getName())) { - return; - } - - if (!$type->isBuiltin()) { - $namespace->addUse($type->getName()); - } - } - - $parameter = $method->addParameter($reflectionParameter->getName()); - $parameter->setType(implode('|', $types)); - - if (!$reflectionParameter->isDefaultValueAvailable()) { - $parameter->setNullable($reflectionParameter->isOptional()); - } - - if (count($types) > 1 || $reflectionParameter->isVariadic()) { - $parameter->setNullable(false); - } - - if (!$reflectionParameter->isDefaultValueAvailable()) { - return; - } - - $defaultValue = $reflectionParameter->getDefaultValue(); - if (is_object($defaultValue)) { - $parameter->setDefaultValue(null); - $parameter->setNullable(true); - - return; - } - - $parameter->setDefaultValue($defaultValue); - $parameter->setNullable(false); - } - - private function isExcludedType(string $typeName): bool - { - foreach ($this->excludedTypePrefixes as $excludedPrefix) { - if (str_starts_with($typeName, $excludedPrefix)) { - return true; - } - } - - return in_array($typeName, $this->excludedTypeNames, true); - } -} diff --git a/src-dev/CodeGen/FluentBuilder/Mixin.php b/src-dev/CodeGen/FluentBuilder/Mixin.php deleted file mode 100644 index 0e211711c..000000000 --- a/src-dev/CodeGen/FluentBuilder/Mixin.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Attribute; - -#[Attribute(Attribute::TARGET_CLASS)] -final readonly class Mixin -{ - /** - * @param array $exclude - * @param array $include - */ - public function __construct( - public string|null $prefix = null, - public bool $prefixParameter = false, - public bool $requireInclusion = false, - public array $exclude = [], - public array $include = [], - ) { - } -} diff --git a/src-dev/CodeGen/FluentBuilder/MixinGenerator.php b/src-dev/CodeGen/FluentBuilder/MixinGenerator.php deleted file mode 100644 index 3f605873a..000000000 --- a/src-dev/CodeGen/FluentBuilder/MixinGenerator.php +++ /dev/null @@ -1,253 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\PhpNamespace; -use Nette\PhpGenerator\Printer; -use ReflectionClass; -use ReflectionParameter; -use Respect\Dev\CodeGen\CodeGenerator; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\InterfaceConfig; -use Respect\Dev\CodeGen\NamespaceScanner; - -use function file_get_contents; -use function in_array; -use function is_file; -use function is_readable; -use function ksort; - -final class MixinGenerator implements CodeGenerator -{ - /** @param array $interfaces */ - public function __construct( - private readonly Config $config, - private readonly MethodBuilder $methodBuilder = new MethodBuilder(), - private readonly array $interfaces = [], - ) { - } - - /** @return array filename => content */ - public function generate(): array - { - $nodes = NamespaceScanner::scan($this->config->sourceDir, $this->config->sourceNamespace); - $prefixes = $this->discoverPrefixes($nodes); - $filters = $this->discoverFilters($nodes); - - $files = []; - - foreach ($this->interfaces as $interfaceConfig) { - $prefixInterfaceNames = []; - - foreach ($prefixes as $prefix) { - $interfaceName = $prefix['name'] . $interfaceConfig->suffix; - $prefixInterfaceNames[] = $this->config->outputNamespace . '\\' . $interfaceName; - - $this->generateInterface( - $interfaceName, - $interfaceConfig, - $nodes, - $filters, - $prefix, - $files, - ); - } - - $this->generateRootInterface( - $interfaceConfig, - $prefixInterfaceNames, - $nodes, - $filters, - $files, - ); - } - - return $files; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverPrefixes(array $nodes): array - { - $prefixes = []; - - foreach ($nodes as $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $mixin = $attributes[0]->newInstance(); - if ($mixin->prefix === null) { - continue; - } - - $constructor = $reflection->getConstructor(); - $prefixParameter = null; - - if ($mixin->prefixParameter && $constructor !== null) { - $parameters = $constructor->getParameters(); - if ($parameters !== []) { - $prefixParameter = $parameters[0]; - } - } - - $prefixes[$mixin->prefix] = [ - 'name' => $reflection->getShortName(), - 'prefix' => $mixin->prefix, - 'requireInclusion' => $mixin->requireInclusion, - 'prefixParameter' => $prefixParameter, - ]; - } - - ksort($prefixes); - - return $prefixes; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverFilters(array $nodes): array - { - $filters = []; - - foreach ($nodes as $name => $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $filters[$name] = $attributes[0]->newInstance(); - } - - return $filters; - } - - /** - * @param array $nodes - * @param array $filters - * @param array{name: string, prefix: string, requireInclusion: bool, prefixParameter: ?ReflectionParameter} $prefix - * @param array $files - */ - private function generateInterface( - string $interfaceName, - InterfaceConfig $config, - array $nodes, - array $filters, - array $prefix, - array &$files, - ): void { - $namespace = new PhpNamespace($this->config->outputNamespace); - $interface = $namespace->addInterface($interfaceName); - - foreach ($nodes as $name => $reflection) { - $mixin = $filters[$name] ?? null; - - if ($prefix['requireInclusion']) { - if ($mixin === null || !in_array($prefix['prefix'], $mixin->include, true)) { - continue; - } - } elseif ($mixin !== null && in_array($prefix['prefix'], $mixin->exclude, true)) { - continue; - } - - $method = $this->methodBuilder->build( - $namespace, - $reflection, - $config->returnType, - $prefix['prefix'], - $config->static, - $prefix['prefixParameter'], - ); - - $interface->addMember($method); - } - - $this->addFile($interfaceName, $namespace, $files); - } - - /** - * @param array $prefixInterfaceNames - * @param array $nodes - * @param array $filters - * @param array $files - */ - private function generateRootInterface( - InterfaceConfig $config, - array $prefixInterfaceNames, - array $nodes, - array $filters, - array &$files, - ): void { - $interfaceName = $config->suffix; - $namespace = new PhpNamespace($this->config->outputNamespace); - $interface = $namespace->addInterface($interfaceName); - - foreach ($config->rootExtends as $extend) { - $namespace->addUse($extend); - $interface->addExtend($extend); - } - - foreach ($prefixInterfaceNames as $prefixInterfaceName) { - $namespace->addUse($prefixInterfaceName); - $interface->addExtend($prefixInterfaceName); - } - - if ($config->rootComment !== null) { - $interface->addComment($config->rootComment); - } - - foreach ($config->rootUses as $use) { - $namespace->addUse($use); - } - - foreach ($nodes as $reflection) { - $method = $this->methodBuilder->build( - $namespace, - $reflection, - $config->returnType, - null, - $config->static, - ); - - $interface->addMember($method); - } - - $this->addFile($interfaceName, $namespace, $files); - } - - /** @param array $files */ - private function addFile(string $interfaceName, PhpNamespace $namespace, array &$files): void - { - $filename = $this->config->outputDir . '/' . $interfaceName . '.php'; - - $printer = new Printer(); - $printer->wrapLength = 300; - - $existingContent = ''; - if (is_file($filename) && is_readable($filename)) { - $existingContent = file_get_contents($filename) ?: ''; - } - - $formattedContent = $this->config->outputFormatter->format( - $printer->printNamespace($namespace), - $existingContent, - ); - - $files[$filename] = $formattedContent; - } -} diff --git a/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php b/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php deleted file mode 100644 index a91a9c63f..000000000 --- a/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php +++ /dev/null @@ -1,160 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\PhpNamespace; -use Nette\PhpGenerator\Printer; -use ReflectionClass; -use Respect\Dev\CodeGen\CodeGenerator; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\NamespaceScanner; - -use function array_keys; -use function ctype_upper; -use function file_get_contents; -use function is_file; -use function is_readable; -use function ksort; -use function lcfirst; -use function str_starts_with; -use function strlen; -use function uksort; - -final class PrefixMapGenerator implements CodeGenerator -{ - public function __construct( - private readonly Config $config, - private readonly string $outputClassName, - ) { - } - - /** @return array filename => content */ - public function generate(): array - { - $nodes = NamespaceScanner::scan($this->config->sourceDir, $this->config->sourceNamespace); - $prefixes = $this->discoverPrefixes($nodes); - $composable = $this->buildComposable($nodes, $prefixes); - $composableWithArgument = $this->buildComposableWithArgument($prefixes); - - $namespace = new PhpNamespace($this->config->outputNamespace); - $class = $namespace->addClass($this->outputClassName); - $class->setFinal(); - - $class->addConstant('COMPOSABLE', $composable)->setPublic()->setType('array'); - $class->addConstant('COMPOSABLE_WITH_ARGUMENT', $composableWithArgument)->setPublic()->setType('array'); - - $printer = new Printer(); - $printer->wrapLength = 300; - - $outputFile = $this->config->outputDir . '/' . $this->outputClassName . '.php'; - - $existingContent = ''; - if (is_file($outputFile) && is_readable($outputFile)) { - $existingContent = file_get_contents($outputFile) ?: ''; - } - - $formattedContent = $this->config->outputFormatter->format( - $printer->printNamespace($namespace), - $existingContent, - ); - - return [$outputFile => $formattedContent]; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverPrefixes(array $nodes): array - { - $prefixes = []; - - foreach ($nodes as $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $mixin = $attributes[0]->newInstance(); - if ($mixin->prefix === null) { - continue; - } - - $prefixes[$mixin->prefix] = [ - 'prefix' => $mixin->prefix, - 'prefixParameter' => $mixin->prefixParameter, - ]; - } - - ksort($prefixes); - - return $prefixes; - } - - /** - * @param array $nodes - * @param array $prefixes - * - * @return array - */ - private function buildComposable(array $nodes, array $prefixes): array - { - $composable = []; - - foreach (array_keys($prefixes) as $prefix) { - $composable[$prefix] = true; - - foreach (array_keys($nodes) as $name) { - $lcName = lcfirst($name); - if ($lcName === $prefix) { - continue; - } - - if (!str_starts_with($lcName, $prefix)) { - continue; - } - - if (!ctype_upper($lcName[strlen($prefix)])) { - continue; - } - - $composable[$lcName] = true; - } - } - - uksort($composable, static fn(string $a, string $b): int => strlen($b) <=> strlen($a) ?: $a <=> $b); - - return $composable; - } - - /** - * @param array $prefixes - * - * @return array - */ - private function buildComposableWithArgument(array $prefixes): array - { - $composableWithArgument = []; - - foreach ($prefixes as $prefix => $info) { - if (!$info['prefixParameter']) { - continue; - } - - $composableWithArgument[$prefix] = true; - } - - ksort($composableWithArgument); - - return $composableWithArgument; - } -} diff --git a/src-dev/CodeGen/InterfaceConfig.php b/src-dev/CodeGen/InterfaceConfig.php deleted file mode 100644 index 92b16482d..000000000 --- a/src-dev/CodeGen/InterfaceConfig.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -final readonly class InterfaceConfig -{ - /** - * @param array $rootExtends - * @param array $rootUses - */ - public function __construct( - public string $suffix, - public string $returnType, - public bool $static = false, - public array $rootExtends = [], - public string|null $rootComment = null, - public array $rootUses = [], - ) { - } -} diff --git a/src-dev/CodeGen/NamespaceScanner.php b/src-dev/CodeGen/NamespaceScanner.php deleted file mode 100644 index 5f9066aa4..000000000 --- a/src-dev/CodeGen/NamespaceScanner.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -use DirectoryIterator; -use ReflectionClass; - -use function ksort; - -final class NamespaceScanner -{ - /** @return array */ - public static function scan(string $directory, string $namespace): array - { - $nodes = []; - - foreach (new DirectoryIterator($directory) as $file) { - if (!$file->isFile()) { - continue; - } - - $className = $namespace . '\\' . $file->getBasename('.php'); - $reflection = new ReflectionClass($className); - - if ($reflection->isAbstract()) { - continue; - } - - $nodes[$reflection->getShortName()] = $reflection; - } - - ksort($nodes); - - return $nodes; - } -} diff --git a/src-dev/CodeGen/OutputFormatter.php b/src-dev/CodeGen/OutputFormatter.php deleted file mode 100644 index 9b9066ce8..000000000 --- a/src-dev/CodeGen/OutputFormatter.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -use function array_keys; -use function array_values; -use function implode; -use function preg_match; -use function preg_replace; -use function trim; - -use const PHP_EOL; - -final class OutputFormatter -{ - public function format(string $content, string $existingContent): string - { - preg_match('/^<\?php\s*\/\*[\s\S]*?\*\//', $existingContent, $matches); - $existingHeader = $matches[0] ?? ''; - - $replacements = [ - '/\n\n\t(public|private|\/\*\*)/m' => PHP_EOL . ' $1', - '/\t/m' => ' ', - '/\?([a-zA-Z]+) \$/' => '$1|null $', - '/\/\*\*\n +\* (.+)\n +\*\//m' => '/** $1 */', - ]; - - return implode(PHP_EOL, [ - trim($existingHeader) . PHP_EOL, - 'declare(strict_types=1);', - '', - preg_replace( - array_keys($replacements), - array_values($replacements), - $content, - ), - ]); - } -} diff --git a/src-dev/Commands/LintMixinCommand.php b/src-dev/Commands/LintMixinCommand.php index d9c6828de..c9fb1db2d 100644 --- a/src-dev/Commands/LintMixinCommand.php +++ b/src-dev/Commands/LintMixinCommand.php @@ -11,13 +11,14 @@ namespace Respect\Dev\Commands; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\FluentBuilder\MethodBuilder; -use Respect\Dev\CodeGen\FluentBuilder\MixinGenerator; -use Respect\Dev\CodeGen\FluentBuilder\PrefixMapGenerator; -use Respect\Dev\CodeGen\InterfaceConfig; use Respect\Dev\Differ\ConsoleDiffer; use Respect\Dev\Differ\Item; +use Respect\FluentGen\Config; +use Respect\FluentGen\Fluent\InterfaceConfig; +use Respect\FluentGen\Fluent\MethodBuilder; +use Respect\FluentGen\Fluent\MixinGenerator; +use Respect\FluentGen\Fluent\PrefixConstantsGenerator; +use Respect\FluentGen\NamespaceScanner; use Respect\Validation\Mixins\Chain; use Respect\Validation\Validator; use Respect\Validation\ValidatorBuilder; @@ -67,8 +68,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int outputNamespace: 'Respect\\Validation\\Mixins', ); + $scanner = new NamespaceScanner(); + $generator = new MixinGenerator( config: $config, + scanner: $scanner, methodBuilder: new MethodBuilder( excludedTypePrefixes: ['Sokil', 'Egulias'], excludedTypeNames: ['finfo'], @@ -89,9 +93,10 @@ interfaces: [ ], ); - $prefixMapGenerator = new PrefixMapGenerator( + $prefixMapGenerator = new PrefixConstantsGenerator( config: $config, - outputClassName: 'PrefixMap', + scanner: $scanner, + outputClassName: 'PrefixConstants', ); $files = $generator->generate() + $prefixMapGenerator->generate(); diff --git a/src/ContainerRegistry.php b/src/ContainerRegistry.php index 0dcd8c536..88200ce3d 100644 --- a/src/ContainerRegistry.php +++ b/src/ContainerRegistry.php @@ -14,6 +14,10 @@ use DI\Container; use libphonenumber\PhoneNumberUtil; use Psr\Container\ContainerInterface; +use Respect\Fluent\Factories\ComposingLookup; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\ComposableMap; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\StringFormatter\BypassTranslator; use Respect\StringFormatter\Modifier; use Respect\StringFormatter\Modifiers\FormatterModifier; @@ -40,13 +44,14 @@ use Respect\Validation\Message\Parameters\ResultHandler; use Respect\Validation\Message\Renderer; use Respect\Validation\Message\TemplateRegistry; -use Respect\Validation\Transformers\Prefix; -use Respect\Validation\Transformers\Transformer; +use Respect\Validation\Mixins\PrefixConstants; use Symfony\Contracts\Translation\TranslatorInterface; +use function array_map; use function DI\autowire; use function DI\create; use function DI\factory; +use function trim; final class ContainerRegistry { @@ -57,7 +62,6 @@ public static function createContainer(array $definitions = []): Container { return new Container($definitions + [ PhoneNumberUtil::class => factory(static fn() => PhoneNumberUtil::getInstance()), - Transformer::class => create(Prefix::class), TemplateRegistry::class => create(TemplateRegistry::class), TemplateResolver::class => autowire(TemplateResolver::class), TranslatorInterface::class => autowire(BypassTranslator::class), @@ -68,10 +72,26 @@ public static function createContainer(array $definitions = []): Container 'respect.validation.formatter.messages' => autowire(NestedArrayFormatter::class), 'respect.validation.ignored_backtrace_paths' => [__DIR__ . '/ValidatorBuilder.php'], 'respect.validation.rule_factory.namespaces' => ['Respect\\Validation\\Validators'], - ValidatorFactory::class => factory(static fn(Container $container) => new NamespacedValidatorFactory( - $container->get(Transformer::class), - $container->get('respect.validation.rule_factory.namespaces'), - )), + ValidatorFactory::class => factory(static function (Container $container) { + $namespaces = array_map( + static fn($ns) => trim($ns, '\\'), + $container->get('respect.validation.rule_factory.namespaces'), + ); + + return new FluentValidatorFactory( + new ComposingLookup( + new NamespaceLookup( + new Ucfirst(), + Validator::class, + ...$namespaces, + ), + new ComposableMap( + PrefixConstants::COMPOSABLE, + PrefixConstants::COMPOSABLE_WITH_ARGUMENT, + ), + ), + ); + }), Quoter::class => create(CodeQuoter::class)->constructor(120), Handler::class => factory(static function (Container $container) { $handler = CompositeHandler::create(); diff --git a/src/FluentValidatorFactory.php b/src/FluentValidatorFactory.php new file mode 100644 index 000000000..e1b0373f9 --- /dev/null +++ b/src/FluentValidatorFactory.php @@ -0,0 +1,53 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use Respect\Fluent\Exceptions\CouldNotCreate; +use Respect\Fluent\Exceptions\CouldNotResolve; +use Respect\Fluent\FluentFactory; +use Respect\Validation\Exceptions\ComponentException; +use Respect\Validation\Exceptions\InvalidClassException; + +use function sprintf; +use function trim; + +final readonly class FluentValidatorFactory implements ValidatorFactory +{ + public function __construct( + private FluentFactory $factory, + ) { + } + + /** @param array $arguments */ + public function create(string $ruleName, array $arguments = []): Validator + { + try { + $instance = $this->factory->create($ruleName, $arguments); + } catch (CouldNotResolve $e) { + throw new ComponentException(sprintf('"%s" is not a valid rule name', $ruleName), 0, $e); + } catch (CouldNotCreate $e) { + throw new InvalidClassException($e->getMessage(), 0, $e); + } + + if (!$instance instanceof Validator) { + throw new InvalidClassException( + sprintf('"%s" must be an instance of "%s"', $ruleName, Validator::class), + ); + } + + return $instance; + } + + public function withNamespace(string $rulesNamespace): self + { + return new self($this->factory->withNamespace(trim($rulesNamespace, '\\'))); + } +} diff --git a/src/Mixins/Builder.php b/src/Mixins/Builder.php index d4042eda0..431dabd04 100644 --- a/src/Mixins/Builder.php +++ b/src/Mixins/Builder.php @@ -182,11 +182,11 @@ public static function iterableVal(): Chain; public static function json(): Chain; - public static function key(string|int $key, Validator $validator): Chain; + public static function key(int|string $key, Validator $validator): Chain; - public static function keyExists(string|int $key): Chain; + public static function keyExists(int|string $key): Chain; - public static function keyOptional(string|int $key, Validator $validator): Chain; + public static function keyOptional(int|string $key, Validator $validator): Chain; public static function keySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/Chain.php b/src/Mixins/Chain.php index acc75aef5..850938790 100644 --- a/src/Mixins/Chain.php +++ b/src/Mixins/Chain.php @@ -184,11 +184,11 @@ public function iterableVal(): Chain; public function json(): Chain; - public function key(string|int $key, Validator $validator): Chain; + public function key(int|string $key, Validator $validator): Chain; - public function keyExists(string|int $key): Chain; + public function keyExists(int|string $key): Chain; - public function keyOptional(string|int $key, Validator $validator): Chain; + public function keyOptional(int|string $key, Validator $validator): Chain; public function keySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NotBuilder.php b/src/Mixins/NotBuilder.php index 41c6c44ab..225a5e009 100644 --- a/src/Mixins/NotBuilder.php +++ b/src/Mixins/NotBuilder.php @@ -179,11 +179,11 @@ public static function notIterableVal(): Chain; public static function notJson(): Chain; - public static function notKey(string|int $key, Validator $validator): Chain; + public static function notKey(int|string $key, Validator $validator): Chain; - public static function notKeyExists(string|int $key): Chain; + public static function notKeyExists(int|string $key): Chain; - public static function notKeyOptional(string|int $key, Validator $validator): Chain; + public static function notKeyOptional(int|string $key, Validator $validator): Chain; public static function notKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NotChain.php b/src/Mixins/NotChain.php index 113737fac..8ea5fd991 100644 --- a/src/Mixins/NotChain.php +++ b/src/Mixins/NotChain.php @@ -179,11 +179,11 @@ public function notIterableVal(): Chain; public function notJson(): Chain; - public function notKey(string|int $key, Validator $validator): Chain; + public function notKey(int|string $key, Validator $validator): Chain; - public function notKeyExists(string|int $key): Chain; + public function notKeyExists(int|string $key): Chain; - public function notKeyOptional(string|int $key, Validator $validator): Chain; + public function notKeyOptional(int|string $key, Validator $validator): Chain; public function notKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NullOrBuilder.php b/src/Mixins/NullOrBuilder.php index 12f54b0b2..e32cd0939 100644 --- a/src/Mixins/NullOrBuilder.php +++ b/src/Mixins/NullOrBuilder.php @@ -179,11 +179,11 @@ public static function nullOrIterableVal(): Chain; public static function nullOrJson(): Chain; - public static function nullOrKey(string|int $key, Validator $validator): Chain; + public static function nullOrKey(int|string $key, Validator $validator): Chain; - public static function nullOrKeyExists(string|int $key): Chain; + public static function nullOrKeyExists(int|string $key): Chain; - public static function nullOrKeyOptional(string|int $key, Validator $validator): Chain; + public static function nullOrKeyOptional(int|string $key, Validator $validator): Chain; public static function nullOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NullOrChain.php b/src/Mixins/NullOrChain.php index 329659911..66e87b606 100644 --- a/src/Mixins/NullOrChain.php +++ b/src/Mixins/NullOrChain.php @@ -179,11 +179,11 @@ public function nullOrIterableVal(): Chain; public function nullOrJson(): Chain; - public function nullOrKey(string|int $key, Validator $validator): Chain; + public function nullOrKey(int|string $key, Validator $validator): Chain; - public function nullOrKeyExists(string|int $key): Chain; + public function nullOrKeyExists(int|string $key): Chain; - public function nullOrKeyOptional(string|int $key, Validator $validator): Chain; + public function nullOrKeyOptional(int|string $key, Validator $validator): Chain; public function nullOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/PrefixConstants.php b/src/Mixins/PrefixConstants.php new file mode 100644 index 000000000..9afcf99e2 --- /dev/null +++ b/src/Mixins/PrefixConstants.php @@ -0,0 +1,56 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Mixins; + +final class PrefixConstants +{ + public const array COMPOSABLE = [ + 'propertyOptional' => true, + 'propertyExists' => true, + 'keyOptional' => true, + 'keyExists' => true, + 'property' => true, + 'undefOr' => true, + 'keySet' => true, + 'length' => true, + 'nullOr' => true, + 'allOf' => true, + 'all' => true, + 'key' => true, + 'max' => true, + 'min' => true, + 'not' => true, + ]; + public const array COMPOSABLE_WITH_ARGUMENT = ['key' => true, 'property' => true]; + public const array FORBIDDEN = [ + 'All' => ['all' => true], + 'Attributes' => ['all' => true, 'key' => true, 'not' => true, 'property' => true, 'undefOr' => true], + 'Blank' => ['nullOr' => true, 'undefOr' => true], + 'Exists' => ['all' => true, 'key' => true, 'property' => true], + 'Formatted' => ['all' => true, 'key' => true, 'property' => true], + 'Key' => ['all' => true, 'key' => true, 'property' => true], + 'KeyExists' => ['all' => true, 'key' => true, 'property' => true], + 'KeyOptional' => ['all' => true, 'key' => true, 'property' => true], + 'KeySet' => ['all' => true, 'key' => true, 'property' => true], + 'Length' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Max' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Min' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Named' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Not' => ['not' => true], + 'NullOr' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Property' => ['all' => true, 'key' => true, 'property' => true], + 'PropertyExists' => ['all' => true, 'key' => true, 'property' => true], + 'PropertyOptional' => ['all' => true, 'key' => true, 'property' => true], + 'Templated' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Undef' => ['nullOr' => true, 'undefOr' => true], + 'UndefOr' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + ]; +} diff --git a/src/Mixins/PrefixMap.php b/src/Mixins/PrefixMap.php deleted file mode 100644 index d27356be4..000000000 --- a/src/Mixins/PrefixMap.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Validation\Mixins; - -final class PrefixMap -{ - public const array COMPOSABLE = [ - 'propertyOptional' => true, - 'propertyExists' => true, - 'keyOptional' => true, - 'keyExists' => true, - 'property' => true, - 'undefOr' => true, - 'keySet' => true, - 'length' => true, - 'nullOr' => true, - 'allOf' => true, - 'all' => true, - 'key' => true, - 'max' => true, - 'min' => true, - 'not' => true, - ]; - public const array COMPOSABLE_WITH_ARGUMENT = ['key' => true, 'property' => true]; -} diff --git a/src/Mixins/UndefOrBuilder.php b/src/Mixins/UndefOrBuilder.php index c8c6273ec..c03840e6b 100644 --- a/src/Mixins/UndefOrBuilder.php +++ b/src/Mixins/UndefOrBuilder.php @@ -177,11 +177,11 @@ public static function undefOrIterableVal(): Chain; public static function undefOrJson(): Chain; - public static function undefOrKey(string|int $key, Validator $validator): Chain; + public static function undefOrKey(int|string $key, Validator $validator): Chain; - public static function undefOrKeyExists(string|int $key): Chain; + public static function undefOrKeyExists(int|string $key): Chain; - public static function undefOrKeyOptional(string|int $key, Validator $validator): Chain; + public static function undefOrKeyOptional(int|string $key, Validator $validator): Chain; public static function undefOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/UndefOrChain.php b/src/Mixins/UndefOrChain.php index aee52fcae..7d04d2463 100644 --- a/src/Mixins/UndefOrChain.php +++ b/src/Mixins/UndefOrChain.php @@ -177,11 +177,11 @@ public function undefOrIterableVal(): Chain; public function undefOrJson(): Chain; - public function undefOrKey(string|int $key, Validator $validator): Chain; + public function undefOrKey(int|string $key, Validator $validator): Chain; - public function undefOrKeyExists(string|int $key): Chain; + public function undefOrKeyExists(int|string $key): Chain; - public function undefOrKeyOptional(string|int $key, Validator $validator): Chain; + public function undefOrKeyOptional(int|string $key, Validator $validator): Chain; public function undefOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Transformers/Prefix.php b/src/Transformers/Prefix.php index d6205bb84..6c33600cf 100644 --- a/src/Transformers/Prefix.php +++ b/src/Transformers/Prefix.php @@ -11,7 +11,7 @@ namespace Respect\Validation\Transformers; -use Respect\Validation\Mixins\PrefixMap; +use Respect\Validation\Mixins\PrefixConstants; use function array_keys; use function array_slice; @@ -30,7 +30,7 @@ public function transform(ValidatorSpec $validatorSpec): ValidatorSpec return $validatorSpec; } - if (!isset(PrefixMap::COMPOSABLE_WITH_ARGUMENT[$matches['prefix']])) { + if (!isset(PrefixConstants::COMPOSABLE_WITH_ARGUMENT[$matches['prefix']])) { return new ValidatorSpec( $matches['suffix'], $validatorSpec->arguments, @@ -48,7 +48,7 @@ public function transform(ValidatorSpec $validatorSpec): ValidatorSpec /** @return array{}|array{prefix: string, suffix: string} */ private function match(ValidatorSpec $validatorSpec): array { - if ($validatorSpec->wrapper !== null || isset(PrefixMap::COMPOSABLE[$validatorSpec->name])) { + if ($validatorSpec->wrapper !== null || isset(PrefixConstants::COMPOSABLE[$validatorSpec->name])) { return []; } @@ -65,7 +65,7 @@ private static function getRegex(): string { return self::$regex ?? self::$regex = sprintf( '/^(?%s)(?.+)$/', - implode('|', array_keys(PrefixMap::COMPOSABLE)), + implode('|', array_keys(PrefixConstants::COMPOSABLE)), ); } } diff --git a/src/ValidatorBuilder.php b/src/ValidatorBuilder.php index 5c279989f..d4af62934 100644 --- a/src/ValidatorBuilder.php +++ b/src/ValidatorBuilder.php @@ -12,12 +12,20 @@ namespace Respect\Validation; +use Respect\Fluent\Attributes\AssuranceAssertion; +use Respect\Fluent\Attributes\AssuranceParameter; +use Respect\Fluent\Attributes\FluentNamespace; +use Respect\Fluent\Factories\ComposingLookup; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\ComposableMap; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\Validation\Exceptions\ComponentException; use Respect\Validation\Exceptions\ValidationException; use Respect\Validation\Message\ArrayFormatter; use Respect\Validation\Message\Renderer; use Respect\Validation\Message\StringFormatter; use Respect\Validation\Mixins\Builder; +use Respect\Validation\Mixins\PrefixConstants; use Respect\Validation\Validators\AllOf; use Respect\Validation\Validators\Core\Nameable; use Respect\Validation\Validators\Core\ShortCircuitable; @@ -31,6 +39,14 @@ use function is_string; /** @mixin Builder */ +#[FluentNamespace(new ComposingLookup( + new NamespaceLookup(new Ucfirst(), Validator::class, 'Respect\\Validation\\Validators'), + new ComposableMap( + PrefixConstants::COMPOSABLE, + PrefixConstants::COMPOSABLE_WITH_ARGUMENT, + PrefixConstants::FORBIDDEN, + ), +))] final readonly class ValidatorBuilder implements Nameable, ShortCircuitable { /** @var array */ @@ -81,20 +97,31 @@ public function validate(mixed $input, array|string|null $template = null): Resu return $this->toResultQuery($this->evaluate($input), $template); } - public function isValid(mixed $input): bool - { + #[AssuranceAssertion] + public function isValid( + #[AssuranceParameter] + mixed $input, + ): bool { return $this->evaluateShortCircuit($input)->hasPassed; } /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ - public function check(mixed $input, array|string|Throwable|callable|null $template = null): void - { + #[AssuranceAssertion] + public function check( + #[AssuranceParameter] + mixed $input, + array|string|Throwable|callable|null $template = null, + ): void { $this->throwOnFailure($this->evaluateShortCircuit($input), $template); } /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ - public function assert(mixed $input, array|string|Throwable|callable|null $template = null): void - { + #[AssuranceAssertion] + public function assert( + #[AssuranceParameter] + mixed $input, + array|string|Throwable|callable|null $template = null, + ): void { $this->throwOnFailure($this->evaluate($input), $template); } diff --git a/src/Validators/All.php b/src/Validators/All.php index 6a7d728b5..4080e74cb 100644 --- a/src/Validators/All.php +++ b/src/Validators/All.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Path; @@ -23,7 +23,7 @@ use Respect\Validation\Validators\Core\FilteredArray; use Respect\Validation\Validators\Core\ShortCircuitable; -#[Mixin(prefix: 'all', exclude: ['all'])] +#[Composable(prefix: self::class, without: [self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('Every item in', 'Every item in')] final class All extends FilteredArray implements ShortCircuitable diff --git a/src/Validators/Attributes.php b/src/Validators/Attributes.php index 78ff90583..13210b052 100644 --- a/src/Validators/Attributes.php +++ b/src/Validators/Attributes.php @@ -17,13 +17,13 @@ use ReflectionClass; use ReflectionObject; use ReflectionProperty; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Id; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\Reducer; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class Attributes implements Validator { diff --git a/src/Validators/Between.php b/src/Validators/Between.php index 7ef768e25..862dd6a86 100644 --- a/src/Validators/Between.php +++ b/src/Validators/Between.php @@ -15,13 +15,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanCompareValues; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be between {{minValue}} and {{maxValue}}', diff --git a/src/Validators/BetweenExclusive.php b/src/Validators/BetweenExclusive.php index 41ee52218..3fd65054c 100644 --- a/src/Validators/BetweenExclusive.php +++ b/src/Validators/BetweenExclusive.php @@ -12,13 +12,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanCompareValues; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than {{minValue}} and less than {{maxValue}}', diff --git a/src/Validators/Blank.php b/src/Validators/Blank.php index 067864ce6..c83737ba6 100644 --- a/src/Validators/Blank.php +++ b/src/Validators/Blank.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -27,7 +27,7 @@ use function is_string; use function trim; -#[Mixin(exclude: ['nullOr', 'undefOr'])] +#[Composable(without: [NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be blank', diff --git a/src/Validators/Equals.php b/src/Validators/Equals.php index d998f5df0..04ab4932a 100644 --- a/src/Validators/Equals.php +++ b/src/Validators/Equals.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use function is_scalar; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be equal to {{compareTo}}', diff --git a/src/Validators/Equivalent.php b/src/Validators/Equivalent.php index fc3cf6247..a9c41a8b7 100644 --- a/src/Validators/Equivalent.php +++ b/src/Validators/Equivalent.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; use function is_scalar; use function mb_strtoupper; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be equivalent to {{compareTo}}', diff --git a/src/Validators/Even.php b/src/Validators/Even.php index 5208abf90..9ed4c8c4f 100644 --- a/src/Validators/Even.php +++ b/src/Validators/Even.php @@ -18,7 +18,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,7 +26,7 @@ use const FILTER_VALIDATE_INT; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an even number', diff --git a/src/Validators/Exists.php b/src/Validators/Exists.php index dee5bab98..c9b005ef4 100644 --- a/src/Validators/Exists.php +++ b/src/Validators/Exists.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -23,7 +23,7 @@ use function file_exists; use function is_string; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an existing file', diff --git a/src/Validators/Factor.php b/src/Validators/Factor.php index bfdcdcacc..9428d6c3e 100644 --- a/src/Validators/Factor.php +++ b/src/Validators/Factor.php @@ -14,7 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -24,7 +24,7 @@ use function is_numeric; use function preg_match; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a factor of {{dividend|raw}}', diff --git a/src/Validators/Finite.php b/src/Validators/Finite.php index 9c917861b..1b79d3944 100644 --- a/src/Validators/Finite.php +++ b/src/Validators/Finite.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_finite; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a finite number', diff --git a/src/Validators/Formatted.php b/src/Validators/Formatted.php index 23696da5c..eda66f220 100644 --- a/src/Validators/Formatted.php +++ b/src/Validators/Formatted.php @@ -11,12 +11,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\StringFormatter\Formatter; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Formatted implements Validator { diff --git a/src/Validators/GreaterThan.php b/src/Validators/GreaterThan.php index cafa657b5..af6f5cf9a 100644 --- a/src/Validators/GreaterThan.php +++ b/src/Validators/GreaterThan.php @@ -15,11 +15,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than {{compareTo}}', diff --git a/src/Validators/GreaterThanOrEqual.php b/src/Validators/GreaterThanOrEqual.php index 0d273fc23..41a22a2ac 100644 --- a/src/Validators/GreaterThanOrEqual.php +++ b/src/Validators/GreaterThanOrEqual.php @@ -14,11 +14,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than or equal to {{compareTo}}', diff --git a/src/Validators/Identical.php b/src/Validators/Identical.php index 25380531e..2fed245c8 100644 --- a/src/Validators/Identical.php +++ b/src/Validators/Identical.php @@ -15,12 +15,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be identical to {{compareTo}}', diff --git a/src/Validators/In.php b/src/Validators/In.php index 53d46d9b7..845626195 100644 --- a/src/Validators/In.php +++ b/src/Validators/In.php @@ -16,7 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -25,7 +25,7 @@ use function is_array; use function mb_strpos; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be in {{haystack}}', diff --git a/src/Validators/Infinite.php b/src/Validators/Infinite.php index 34bcc67cd..0d7ff13a3 100644 --- a/src/Validators/Infinite.php +++ b/src/Validators/Infinite.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_infinite; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an infinite number', diff --git a/src/Validators/Key.php b/src/Validators/Key.php index f7ac017ce..0308d1b5d 100644 --- a/src/Validators/Key.php +++ b/src/Validators/Key.php @@ -16,17 +16,19 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; +use Respect\Fluent\Attributes\ComposableParameter; use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\KeyRelated; -#[Mixin(prefix: 'key', prefixParameter: true, exclude: ['all', 'key', 'property'])] +#[Composable(prefix: self::class, without: [All::class, self::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Key implements KeyRelated { public function __construct( + #[ComposableParameter] private int|string $key, private Validator $validator, ) { diff --git a/src/Validators/KeyExists.php b/src/Validators/KeyExists.php index c8e0a73aa..f7bf8462b 100644 --- a/src/Validators/KeyExists.php +++ b/src/Validators/KeyExists.php @@ -13,7 +13,7 @@ use ArrayAccess; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; @@ -23,7 +23,7 @@ use function array_key_exists; use function is_array; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be present', diff --git a/src/Validators/KeyOptional.php b/src/Validators/KeyOptional.php index 3bd94596b..d802151e2 100644 --- a/src/Validators/KeyOptional.php +++ b/src/Validators/KeyOptional.php @@ -13,12 +13,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\KeyRelated; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class KeyOptional implements KeyRelated { diff --git a/src/Validators/KeySet.php b/src/Validators/KeySet.php index 499a66238..5f878d044 100644 --- a/src/Validators/KeySet.php +++ b/src/Validators/KeySet.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; @@ -33,7 +33,7 @@ use function array_merge; use function array_slice; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} validation failed', diff --git a/src/Validators/Length.php b/src/Validators/Length.php index feafa0ee6..5f7eead39 100644 --- a/src/Validators/Length.php +++ b/src/Validators/Length.php @@ -20,7 +20,7 @@ use Attribute; use Countable as PhpCountable; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -30,7 +30,7 @@ use function is_string; use function mb_strlen; -#[Mixin(prefix: 'length', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'The length of', diff --git a/src/Validators/LessThan.php b/src/Validators/LessThan.php index eb2039b69..715d98337 100644 --- a/src/Validators/LessThan.php +++ b/src/Validators/LessThan.php @@ -15,11 +15,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be less than {{compareTo}}', diff --git a/src/Validators/LessThanOrEqual.php b/src/Validators/LessThanOrEqual.php index 7734befb5..28e22730a 100644 --- a/src/Validators/LessThanOrEqual.php +++ b/src/Validators/LessThanOrEqual.php @@ -14,11 +14,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be less than or equal to {{compareTo}}', diff --git a/src/Validators/Max.php b/src/Validators/Max.php index 9d89607bb..d31f30374 100644 --- a/src/Validators/Max.php +++ b/src/Validators/Max.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use function max; -#[Mixin(prefix: 'max', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('The maximum of', 'The maximum of')] final class Max extends FilteredArray diff --git a/src/Validators/Min.php b/src/Validators/Min.php index 9083b43c8..6d1812cbf 100644 --- a/src/Validators/Min.php +++ b/src/Validators/Min.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use function min; -#[Mixin(prefix: 'min', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('The minimum of', 'The minimum of')] final class Min extends FilteredArray diff --git a/src/Validators/Multiple.php b/src/Validators/Multiple.php index e780ff802..891509ed8 100644 --- a/src/Validators/Multiple.php +++ b/src/Validators/Multiple.php @@ -17,12 +17,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a multiple of {{multipleOf}}', diff --git a/src/Validators/Named.php b/src/Validators/Named.php index db58b9dc4..7da7b0a5c 100644 --- a/src/Validators/Named.php +++ b/src/Validators/Named.php @@ -12,7 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Name; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -20,7 +20,7 @@ use function is_string; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final readonly class Named implements Nameable { diff --git a/src/Validators/Not.php b/src/Validators/Not.php index 1026bb6c6..224342acf 100644 --- a/src/Validators/Not.php +++ b/src/Validators/Not.php @@ -18,11 +18,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(prefix: 'not', exclude: ['not'])] +#[Composable(prefix: self::class, without: [self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final readonly class Not implements Validator { diff --git a/src/Validators/NullOr.php b/src/Validators/NullOr.php index 9b02f1500..072712b68 100644 --- a/src/Validators/NullOr.php +++ b/src/Validators/NullOr.php @@ -14,14 +14,17 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use function array_map; -#[Mixin(prefix: 'nullOr', exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable( + prefix: self::class, + without: [All::class, Key::class, Property::class, Not::class, self::class, UndefOr::class], +)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'or must be null', diff --git a/src/Validators/Odd.php b/src/Validators/Odd.php index bc0331fd7..231b929aa 100644 --- a/src/Validators/Odd.php +++ b/src/Validators/Odd.php @@ -17,7 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,7 +26,7 @@ use const FILTER_VALIDATE_INT; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an odd number', diff --git a/src/Validators/Positive.php b/src/Validators/Positive.php index 748180d63..88b010332 100644 --- a/src/Validators/Positive.php +++ b/src/Validators/Positive.php @@ -15,13 +15,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a positive number', diff --git a/src/Validators/Property.php b/src/Validators/Property.php index 023ab0e71..273f11706 100644 --- a/src/Validators/Property.php +++ b/src/Validators/Property.php @@ -19,16 +19,18 @@ use Attribute; use ReflectionClass; use ReflectionObject; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; +use Respect\Fluent\Attributes\ComposableParameter; use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(prefix: 'property', prefixParameter: true, exclude: ['all', 'key', 'property'])] +#[Composable(prefix: self::class, without: [All::class, Key::class, self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Property implements Validator { public function __construct( + #[ComposableParameter] private string $propertyName, private Validator $validator, ) { diff --git a/src/Validators/PropertyExists.php b/src/Validators/PropertyExists.php index efa55c00e..d207ece8d 100644 --- a/src/Validators/PropertyExists.php +++ b/src/Validators/PropertyExists.php @@ -14,7 +14,7 @@ use Attribute; use ReflectionClass; use ReflectionObject; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; @@ -22,7 +22,7 @@ use function is_object; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be present', diff --git a/src/Validators/PropertyOptional.php b/src/Validators/PropertyOptional.php index 56ed4f54e..519e948ef 100644 --- a/src/Validators/PropertyOptional.php +++ b/src/Validators/PropertyOptional.php @@ -13,11 +13,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class PropertyOptional implements Validator { diff --git a/src/Validators/Templated.php b/src/Validators/Templated.php index 4e02ff847..45fdc4ddd 100644 --- a/src/Validators/Templated.php +++ b/src/Validators/Templated.php @@ -12,11 +12,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final readonly class Templated implements Validator { diff --git a/src/Validators/Undef.php b/src/Validators/Undef.php index 2ea85a48a..19905afaa 100644 --- a/src/Validators/Undef.php +++ b/src/Validators/Undef.php @@ -16,13 +16,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['nullOr', 'undefOr'])] +#[Composable(without: [NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be undefined', diff --git a/src/Validators/UndefOr.php b/src/Validators/UndefOr.php index 576fd2def..ce8610eef 100644 --- a/src/Validators/UndefOr.php +++ b/src/Validators/UndefOr.php @@ -13,7 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -21,7 +21,10 @@ use function array_map; -#[Mixin(prefix: 'undefOr', exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable( + prefix: self::class, + without: [All::class, Key::class, Property::class, Not::class, NullOr::class, self::class], +)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'or must be undefined', diff --git a/src-dev/CodeGen/CodeGenerator.php b/tests/src/Validators/NonPublic.php similarity index 57% rename from src-dev/CodeGen/CodeGenerator.php rename to tests/src/Validators/NonPublic.php index 69007f32d..e2ea2fc25 100644 --- a/src-dev/CodeGen/CodeGenerator.php +++ b/tests/src/Validators/NonPublic.php @@ -8,10 +8,11 @@ declare(strict_types=1); -namespace Respect\Dev\CodeGen; +namespace Respect\Validation\Test\Validators; -interface CodeGenerator +final class NonPublic { - /** @return array filename => content */ - public function generate(): array; + private function __construct() + { + } } diff --git a/tests/unit/FluentValidatorFactoryTest.php b/tests/unit/FluentValidatorFactoryTest.php new file mode 100644 index 000000000..2aa0707a3 --- /dev/null +++ b/tests/unit/FluentValidatorFactoryTest.php @@ -0,0 +1,127 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation; + +use Error; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; +use Respect\Validation\Exceptions\ComponentException; +use Respect\Validation\Exceptions\InvalidClassException; +use Respect\Validation\Test\TestCase; +use Respect\Validation\Test\Validators\Stub; +use Respect\Validation\Test\Validators\Valid; + +#[Group('core')] +#[CoversClass(FluentValidatorFactory::class)] +final class FluentValidatorFactoryTest extends TestCase +{ + private const string TEST_NAMESPACE = 'Respect\\Validation\\Test\\Validators'; + + #[Test] + public function itShouldCreateValidatorByName(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_NAMESPACE), + ); + + self::assertInstanceOf(Valid::class, $factory->create('valid')); + } + + #[Test] + public function itShouldPassArgumentsToConstructor(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_NAMESPACE), + ); + + $validator = $factory->create('stub', [true, false, true]); + + self::assertInstanceOf(Stub::class, $validator); + self::assertSame([true, false, true], $validator->validations); + } + + #[Test] + public function itShouldThrowComponentExceptionWhenRuleIsNotFound(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_NAMESPACE), + ); + + $this->expectException(ComponentException::class); + $this->expectExceptionMessage('"nonExistingRule" is not a valid rule name'); + + $factory->create('nonExistingRule'); + } + + #[Test] + public function itShouldThrowInvalidClassExceptionWhenNonValidatorIsResolved(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), null, self::TEST_NAMESPACE), + ); + + $this->expectException(InvalidClassException::class); + $this->expectExceptionMessage('must be an instance of'); + + $factory->create('invalid'); + } + + #[Test] + public function itShouldBubbleUpErrorWhenInstantiationFails(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), null, self::TEST_NAMESPACE), + ); + + $this->expectException(Error::class); + + $factory->create('myAbstractClass'); + } + + #[Test] + public function itShouldThrowInvalidClassExceptionWhenInstantiationFails(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), null, self::TEST_NAMESPACE), + ); + + $this->expectException(InvalidClassException::class); + + $factory->create('nonPublic'); + } + + #[Test] + public function itShouldPrependNamespaceViaWithNamespace(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, 'NonExistent\\Namespace'), + ); + + $extended = $factory->withNamespace(self::TEST_NAMESPACE); + + self::assertInstanceOf(Valid::class, $extended->create('valid')); + } + + #[Test] + public function itShouldReturnNewInstanceFromWithNamespace(): void + { + $factory = new FluentValidatorFactory( + new NamespaceLookup(new Ucfirst(), Validator::class, self::TEST_NAMESPACE), + ); + + $extended = $factory->withNamespace('Another\\Namespace'); + + self::assertNotSame($factory, $extended); + } +}