diff --git a/src-dev/CodeGen/Attributes/Mixin.php b/src-dev/CodeGen/Attributes/Mixin.php new file mode 100644 index 00000000..35586492 --- /dev/null +++ b/src-dev/CodeGen/Attributes/Mixin.php @@ -0,0 +1,30 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Dev\CodeGen\Attributes; + +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/InterfaceConfig.php b/src-dev/CodeGen/InterfaceConfig.php new file mode 100644 index 00000000..92b16482 --- /dev/null +++ b/src-dev/CodeGen/InterfaceConfig.php @@ -0,0 +1,28 @@ + + */ + +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/MethodBuilder.php b/src-dev/CodeGen/MethodBuilder.php new file mode 100644 index 00000000..6f3fdfe0 --- /dev/null +++ b/src-dev/CodeGen/MethodBuilder.php @@ -0,0 +1,169 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Dev\CodeGen; + +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 $validatorReflection, + string $returnType, + string|null $prefix = null, + bool $static = false, + ReflectionParameter|null $prefixParameter = null, + ): Method { + $originalName = $validatorReflection->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 = $validatorReflection->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/MixinGenerator.php b/src-dev/CodeGen/MixinGenerator.php new file mode 100644 index 00000000..dfb3c956 --- /dev/null +++ b/src-dev/CodeGen/MixinGenerator.php @@ -0,0 +1,279 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Dev\CodeGen; + +use DirectoryIterator; +use Nette\PhpGenerator\PhpNamespace; +use Nette\PhpGenerator\Printer; +use ReflectionClass; +use ReflectionParameter; +use Respect\Dev\CodeGen\Attributes\Mixin; + +use function file_get_contents; +use function in_array; +use function is_file; +use function is_readable; +use function ksort; +use function sprintf; + +final class MixinGenerator +{ + /** @param array $interfaces */ + public function __construct( + private readonly string $sourceDir, + private readonly string $sourceNamespace, + private readonly string $outputDir, + private readonly string $outputNamespace, + private readonly array $interfaces, + private readonly MethodBuilder $methodBuilder = new MethodBuilder(), + private readonly OutputFormatter $outputFormatter = new OutputFormatter(), + ) { + } + + /** @return array filename => content */ + public function generate(): array + { + $validators = $this->scanValidators(); + $prefixes = $this->discoverPrefixes($validators); + $filters = $this->discoverFilters($validators); + + $files = []; + + foreach ($this->interfaces as $interfaceConfig) { + $prefixInterfaceNames = []; + + foreach ($prefixes as $prefix) { + $interfaceName = $prefix['name'] . $interfaceConfig->suffix; + $prefixInterfaceNames[] = $this->outputNamespace . '\\' . $interfaceName; + + $this->generateInterface( + $interfaceName, + $interfaceConfig, + $validators, + $filters, + $prefix, + $files, + ); + } + + $this->generateRootInterface( + $interfaceConfig, + $prefixInterfaceNames, + $validators, + $filters, + $files, + ); + } + + return $files; + } + + /** @return array */ + private function scanValidators(): array + { + $validators = []; + + foreach (new DirectoryIterator($this->sourceDir) as $file) { + if (!$file->isFile()) { + continue; + } + + $className = $this->sourceNamespace . '\\' . $file->getBasename('.php'); + $reflection = new ReflectionClass($className); + + if ($reflection->isAbstract()) { + continue; + } + + $validators[$reflection->getShortName()] = $reflection; + } + + ksort($validators); + + return $validators; + } + + /** + * @param array $validators + * + * @return array + */ + private function discoverPrefixes(array $validators): array + { + $prefixes = []; + + foreach ($validators as $reflection) { + $attributes = $reflection->getAttributes(Mixin::class); + if ($attributes === []) { + continue; + } + + $mixin = $attributes[0]->newInstance(); + if ($mixin->prefix === null) { + continue; + } + + $prefixParameter = null; + if ($mixin->prefixParameter) { + $constructor = $reflection->getConstructor(); + if ($constructor !== null) { + $params = $constructor->getParameters(); + if ($params !== []) { + $prefixParameter = $params[0]; + } + } + } + + $prefixes[$mixin->prefix] = [ + 'name' => $reflection->getShortName(), + 'prefix' => $mixin->prefix, + 'requireInclusion' => $mixin->requireInclusion, + 'prefixParameter' => $prefixParameter, + ]; + } + + ksort($prefixes); + + return $prefixes; + } + + /** + * @param array $validators + * + * @return array + */ + private function discoverFilters(array $validators): array + { + $filters = []; + + foreach ($validators as $name => $reflection) { + $attributes = $reflection->getAttributes(Mixin::class); + if ($attributes === []) { + continue; + } + + $filters[$name] = $attributes[0]->newInstance(); + } + + return $filters; + } + + /** + * @param array $validators + * @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 $validators, + array $filters, + array $prefix, + array &$files, + ): void { + $namespace = new PhpNamespace($this->outputNamespace); + $interface = $namespace->addInterface($interfaceName); + + foreach ($validators 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 $validators + * @param array $filters + * @param array $files + */ + private function generateRootInterface( + InterfaceConfig $config, + array $prefixInterfaceNames, + array $validators, + array $filters, + array &$files, + ): void { + $interfaceName = $config->suffix; + $namespace = new PhpNamespace($this->outputNamespace); + $interface = $namespace->addInterface($interfaceName); + + foreach ($config->rootExtends as $extend) { + $interface->addExtend($extend); + } + + foreach ($prefixInterfaceNames as $prefixInterface) { + $interface->addExtend($prefixInterface); + } + + if ($config->rootComment !== null) { + $interface->addComment($config->rootComment); + } + + foreach ($config->rootUses as $use) { + $namespace->addUse($use); + } + + foreach ($validators 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 + { + $printer = new Printer(); + $printer->wrapLength = 300; + + $filename = sprintf('%s/%s.php', $this->outputDir, $interfaceName); + $existingContent = ''; + if (is_file($filename) && is_readable($filename)) { + $existingContent = file_get_contents($filename) ?: ''; + } + + $formattedContent = $this->outputFormatter->format( + $printer->printNamespace($namespace), + $existingContent, + ); + + $files[$filename] = $formattedContent; + } +} diff --git a/src-dev/CodeGen/OutputFormatter.php b/src-dev/CodeGen/OutputFormatter.php new file mode 100644 index 00000000..9b4d4870 --- /dev/null +++ b/src-dev/CodeGen/OutputFormatter.php @@ -0,0 +1,47 @@ + + */ + +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|\/\*\*)/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 97444004..46a09562 100644 --- a/src-dev/Commands/LintMixinCommand.php +++ b/src-dev/Commands/LintMixinCommand.php @@ -11,36 +11,12 @@ declare(strict_types=1); namespace Respect\Dev\Commands; -use DirectoryIterator; -use Nette\PhpGenerator\InterfaceType; -use Nette\PhpGenerator\Method; -use Nette\PhpGenerator\PhpNamespace; -use Nette\PhpGenerator\Printer; -use ReflectionClass; -use ReflectionNamedType; -use ReflectionParameter; -use ReflectionUnionType; +use Respect\Dev\CodeGen\InterfaceConfig; +use Respect\Dev\CodeGen\MethodBuilder; +use Respect\Dev\CodeGen\MixinGenerator; use Respect\Dev\Differ\ConsoleDiffer; use Respect\Dev\Differ\Item; -use Respect\Validation\Mixins\AllBuilder; -use Respect\Validation\Mixins\AllChain; use Respect\Validation\Mixins\Chain; -use Respect\Validation\Mixins\KeyBuilder; -use Respect\Validation\Mixins\KeyChain; -use Respect\Validation\Mixins\LengthBuilder; -use Respect\Validation\Mixins\LengthChain; -use Respect\Validation\Mixins\MaxBuilder; -use Respect\Validation\Mixins\MaxChain; -use Respect\Validation\Mixins\MinBuilder; -use Respect\Validation\Mixins\MinChain; -use Respect\Validation\Mixins\NotBuilder; -use Respect\Validation\Mixins\NotChain; -use Respect\Validation\Mixins\NullOrBuilder; -use Respect\Validation\Mixins\NullOrChain; -use Respect\Validation\Mixins\PropertyBuilder; -use Respect\Validation\Mixins\PropertyChain; -use Respect\Validation\Mixins\UndefOrBuilder; -use Respect\Validation\Mixins\UndefOrChain; use Respect\Validation\Validator; use Respect\Validation\ValidatorBuilder; use Symfony\Component\Console\Attribute\AsCommand; @@ -48,27 +24,13 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use function array_keys; -use function array_merge; -use function array_values; use function count; use function dirname; use function file_get_contents; use function file_put_contents; -use function implode; -use function in_array; -use function is_object; -use function ksort; -use function lcfirst; -use function preg_match; -use function preg_replace; +use function is_file; +use function is_readable; use function sprintf; -use function str_contains; -use function str_starts_with; -use function trim; -use function ucfirst; - -use const PHP_EOL; #[AsCommand( name: 'lint:mixin', @@ -76,46 +38,6 @@ use const PHP_EOL; )] final class LintMixinCommand extends Command { - private const array NUMBER_RELATED_VALIDATORS = [ - 'Between', - 'BetweenExclusive', - 'Equals', - 'Equivalent', - 'Even', - 'Factor', - 'Fibonacci', - 'Finite', - 'GreaterThan', - 'Identical', - 'In', - 'Infinite', - 'LessThan', - 'LessThanOrEqual', - 'GreaterThanOrEqual', - 'Multiple', - 'Odd', - 'PerfectSquare', - 'Positive', - 'PrimeNumber', - ]; - - private const array STRUCTURE_RELATED_VALIDATORS = [ - 'Exists', - 'Key', - 'KeyExists', - 'KeyOptional', - 'KeySet', - 'NullOr', - 'UndefOr', - 'Property', - 'PropertyExists', - 'PropertyOptional', - 'Attributes', - 'Formatted', - 'Templated', - 'Named', - ]; - public function __construct( private readonly ConsoleDiffer $differ, ) { @@ -134,102 +56,51 @@ final class LintMixinCommand extends Command protected function execute(InputInterface $input, OutputInterface $output): int { - // Scan validators directory $srcDir = dirname(__DIR__, 2) . '/src'; - $validatorsDir = $srcDir . '/Validators'; - $validators = $this->scanValidators($validatorsDir); - // Define mixins - $mixins = [ - ['All', 'all', [], array_merge(['All'], self::STRUCTURE_RELATED_VALIDATORS)], - ['Key', 'key', [], self::STRUCTURE_RELATED_VALIDATORS], - ['Length', 'length', self::NUMBER_RELATED_VALIDATORS, []], - ['Max', 'max', self::NUMBER_RELATED_VALIDATORS, []], - ['Min', 'min', self::NUMBER_RELATED_VALIDATORS, []], - ['Not', 'not', [], ['Not', 'NullOr', 'UndefOr', 'Attributes', 'Templated', 'Named']], - ['NullOr', 'nullOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Templated', 'Named']], - ['Property', 'property', [], self::STRUCTURE_RELATED_VALIDATORS], - ['UndefOr', 'undefOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Attributes', 'Templated', 'Named']], - [null, null, [], []], - ]; + $generator = new MixinGenerator( + sourceDir: $srcDir . '/Validators', + sourceNamespace: 'Respect\\Validation\\Validators', + outputDir: $srcDir . '/Mixins', + outputNamespace: 'Respect\\Validation\\Mixins', + methodBuilder: new MethodBuilder( + excludedTypePrefixes: ['Sokil', 'Egulias'], + excludedTypeNames: ['finfo'], + ), + interfaces: [ + new InterfaceConfig( + suffix: 'Builder', + returnType: Chain::class, + static: true, + ), + new InterfaceConfig( + suffix: 'Chain', + returnType: Chain::class, + rootExtends: [Validator::class], + rootComment: '@mixin ValidatorBuilder', + rootUses: [ValidatorBuilder::class], + ), + ], + ); + + $files = $generator->generate(); $updatableFiles = []; - - foreach ($mixins as [$name, $prefix, $allowList, $denyList]) { - $chainedNamespace = new PhpNamespace('Respect\\Validation\\Mixins'); - $chainedInterface = $chainedNamespace->addInterface($name . 'Chain'); - - $staticNamespace = new PhpNamespace('Respect\\Validation\\Mixins'); - $staticInterface = $staticNamespace->addInterface($name . 'Builder'); - - if ($name === null) { - $chainedInterface->addExtend(Validator::class); - $chainedInterface->addExtend(AllChain::class); - $chainedInterface->addExtend(KeyChain::class); - $chainedInterface->addExtend(LengthChain::class); - $chainedInterface->addExtend(MaxChain::class); - $chainedInterface->addExtend(MinChain::class); - $chainedInterface->addExtend(NotChain::class); - $chainedInterface->addExtend(NullOrChain::class); - $chainedInterface->addExtend(PropertyChain::class); - $chainedInterface->addExtend(UndefOrChain::class); - $chainedInterface->addComment('@mixin ValidatorBuilder'); - $chainedNamespace->addUse(ValidatorBuilder::class); - - $staticInterface->addExtend(AllBuilder::class); - $staticInterface->addExtend(KeyBuilder::class); - $staticInterface->addExtend(LengthBuilder::class); - $staticInterface->addExtend(MaxBuilder::class); - $staticInterface->addExtend(MinBuilder::class); - $staticInterface->addExtend(NotBuilder::class); - $staticInterface->addExtend(NullOrBuilder::class); - $staticInterface->addExtend(PropertyBuilder::class); - $staticInterface->addExtend(UndefOrBuilder::class); + foreach ($files as $filename => $content) { + $existingContent = ''; + if (is_file($filename) && is_readable($filename)) { + $existingContent = file_get_contents($filename) ?: ''; } - foreach ($validators as $originalName => $reflection) { - $this->addMethodToInterface( - $staticNamespace, - $originalName, - $staticInterface, - $reflection, - $prefix, - $allowList, - $denyList, - ); - $this->addMethodToInterface( - $chainedNamespace, - $originalName, - $chainedInterface, - $reflection, - $prefix, - $allowList, - $denyList, - ); + if ($content === $existingContent) { + continue; } - $printer = new Printer(); - $printer->wrapLength = 300; - - foreach ( - [ - [$staticNamespace, $staticInterface], - [$chainedNamespace, $chainedInterface], - ] as [$namespace, $interface] - ) { - $filename = sprintf('%s/Mixins/%s.php', $srcDir, $interface->getName()); - $existingContent = file_get_contents($filename); - $formattedContent = $this->getFormattedContent($printer->printNamespace($namespace), $existingContent); - if ($formattedContent === $existingContent) { - continue; - } - - $updatableFiles[$filename] = $formattedContent; - $output->writeln($this->differ->diff( - new Item($filename, $existingContent), - new Item($filename, $formattedContent), - )); - } + $updatableFiles[$filename] = $content; + $output->writeln($this->differ->diff( + new Item($filename, $existingContent), + new Item($filename, $content), + )); } if ($updatableFiles === []) { @@ -248,167 +119,4 @@ final class LintMixinCommand extends Command return Command::SUCCESS; } - - /** @return array */ - private function scanValidators(string $directory): array - { - $names = []; - - foreach (new DirectoryIterator($directory) as $file) { - if (!$file->isFile()) { - continue; - } - - $className = 'Respect\\Validation\\Validators\\' . $file->getBasename('.php'); - $reflection = new ReflectionClass($className); - - if ($reflection->isAbstract()) { - continue; - } - - $names[$reflection->getShortName()] = $reflection; - } - - ksort($names); - - return $names; - } - - /** - * @param array $allowList - * @param array $denyList - */ - private function addMethodToInterface( - PhpNamespace $namespace, - string $originalName, - InterfaceType $interfaceType, - ReflectionClass $reflection, - string|null $prefix, - array $allowList, - array $denyList, - ): void { - if ($allowList !== [] && !in_array($reflection->getShortName(), $allowList, true)) { - return; - } - - if ($denyList !== [] && in_array($reflection->getShortName(), $denyList, true)) { - return; - } - - $name = $prefix ? $prefix . ucfirst($originalName) : lcfirst($originalName); - $method = $interfaceType->addMethod($name)->setPublic()->setReturnType(Chain::class); - - if (str_contains($interfaceType->getName(), 'Builder')) { - $method->setStatic(); - } - - if ($prefix === 'key') { - $method->addParameter('key')->setType('int|string'); - } - - if ($prefix === 'property') { - $method->addParameter('propertyName')->setType('string'); - } - - $reflectionConstructor = $reflection->getConstructor(); - if ($reflectionConstructor === null) { - return; - } - - $comment = $reflectionConstructor->getDocComment(); - if ($comment) { - $method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $comment)); - } - - foreach ($reflectionConstructor->getParameters() as $reflectionParameter) { - $this->addParameterToMethod($method, $reflectionParameter, $namespace); - } - } - - private function addParameterToMethod( - 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 ( - str_starts_with($type->getName(), 'Sokil') - || str_starts_with($type->getName(), 'Egulias') - || $type->getName() === 'finfo' - ) { - 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 getFormattedContent(string $content, string $existingContent): string - { - preg_match('/^<\?php\s*\/\*[\s\S]*?\*\//', $existingContent, $matches); - $existingHeader = $matches[0] ?? ''; - - $replacements = [ - '/\n\n\t(public|\/\*\*)/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/Validators/All.php b/src/Validators/All.php index 613ad6cf..4fff00f1 100644 --- a/src/Validators/All.php +++ b/src/Validators/All.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Path; @@ -22,6 +23,7 @@ use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use Respect\Validation\Validators\Core\ShortCircuitable; +#[Mixin(prefix: 'all', exclude: ['all'])] #[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 ed16b143..36873c49 100644 --- a/src/Validators/Attributes.php +++ b/src/Validators/Attributes.php @@ -17,11 +17,13 @@ use ReflectionAttribute; use ReflectionClass; use ReflectionObject; use ReflectionProperty; +use Respect\Dev\CodeGen\Attributes\Mixin; 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'])] #[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 b5879325..c3ffac4c 100644 --- a/src/Validators/Between.php +++ b/src/Validators/Between.php @@ -15,11 +15,13 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; 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'])] #[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 0c4d17eb..bf828f66 100644 --- a/src/Validators/BetweenExclusive.php +++ b/src/Validators/BetweenExclusive.php @@ -12,11 +12,13 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; 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'])] #[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 37835be2..3e7e988e 100644 --- a/src/Validators/Blank.php +++ b/src/Validators/Blank.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -26,6 +27,7 @@ use function is_numeric; use function is_string; use function trim; +#[Mixin(exclude: ['nullOr', 'undefOr'])] #[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 4ff68e2f..1fae4f1a 100644 --- a/src/Validators/Equals.php +++ b/src/Validators/Equals.php @@ -15,12 +15,14 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use function is_scalar; +#[Mixin(include: ['length', 'max', 'min'])] #[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 73e2dafc..506312a5 100644 --- a/src/Validators/Equivalent.php +++ b/src/Validators/Equivalent.php @@ -15,12 +15,14 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; use function is_scalar; use function mb_strtoupper; +#[Mixin(include: ['length', 'max', 'min'])] #[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 728d63a4..7e26b97f 100644 --- a/src/Validators/Even.php +++ b/src/Validators/Even.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ use function filter_var; use const FILTER_VALIDATE_INT; +#[Mixin(include: ['length', 'max', 'min'])] #[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 7d491a6c..aec08391 100644 --- a/src/Validators/Exists.php +++ b/src/Validators/Exists.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -22,6 +23,7 @@ use SplFileInfo; use function file_exists; use function is_string; +#[Mixin(exclude: ['all', 'key', 'property'])] #[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 c7ebffb4..0ea328bc 100644 --- a/src/Validators/Factor.php +++ b/src/Validators/Factor.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -23,6 +24,7 @@ use function is_int; use function is_numeric; use function preg_match; +#[Mixin(include: ['length', 'max', 'min'])] #[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 528c5dc6..4016a639 100644 --- a/src/Validators/Finite.php +++ b/src/Validators/Finite.php @@ -15,12 +15,14 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_finite; use function is_numeric; +#[Mixin(include: ['length', 'max', 'min'])] #[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 0425ca73..e47e9c1e 100644 --- a/src/Validators/Formatted.php +++ b/src/Validators/Formatted.php @@ -11,10 +11,12 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\StringFormatter\Formatter; use Respect\Validation\Result; use Respect\Validation\Validator; +#[Mixin(exclude: ['all', 'key', 'property'])] #[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 7e814cec..d3065fac 100644 --- a/src/Validators/GreaterThan.php +++ b/src/Validators/GreaterThan.php @@ -15,9 +15,11 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; +#[Mixin(include: ['length', 'max', 'min'])] #[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 3dd7b83d..979787cc 100644 --- a/src/Validators/GreaterThanOrEqual.php +++ b/src/Validators/GreaterThanOrEqual.php @@ -14,9 +14,11 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; +#[Mixin(include: ['length', 'max', 'min'])] #[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 53abf4e0..3296a770 100644 --- a/src/Validators/Identical.php +++ b/src/Validators/Identical.php @@ -15,10 +15,12 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; +#[Mixin(include: ['length', 'max', 'min'])] #[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 ce07b303..97e45d5b 100644 --- a/src/Validators/In.php +++ b/src/Validators/In.php @@ -16,6 +16,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -24,6 +25,7 @@ use function in_array; use function is_array; use function mb_strpos; +#[Mixin(include: ['length', 'max', 'min'])] #[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 703f9837..599025c8 100644 --- a/src/Validators/Infinite.php +++ b/src/Validators/Infinite.php @@ -15,12 +15,14 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_infinite; use function is_numeric; +#[Mixin(include: ['length', 'max', 'min'])] #[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 c52f40b6..7fbc0c20 100644 --- a/src/Validators/Key.php +++ b/src/Validators/Key.php @@ -16,11 +16,13 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; 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'])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Key implements KeyRelated { diff --git a/src/Validators/KeyExists.php b/src/Validators/KeyExists.php index 6ca02b1b..9be18330 100644 --- a/src/Validators/KeyExists.php +++ b/src/Validators/KeyExists.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use ArrayAccess; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; @@ -22,6 +23,7 @@ use Respect\Validation\Validators\Core\KeyRelated; use function array_key_exists; use function is_array; +#[Mixin(exclude: ['all', 'key', 'property'])] #[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 2276b510..6822bf48 100644 --- a/src/Validators/KeyOptional.php +++ b/src/Validators/KeyOptional.php @@ -13,10 +13,12 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\KeyRelated; +#[Mixin(exclude: ['all', 'key', 'property'])] #[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 562007a7..9e25c435 100644 --- a/src/Validators/KeySet.php +++ b/src/Validators/KeySet.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; @@ -32,6 +33,7 @@ use function array_map; use function array_merge; use function array_slice; +#[Mixin(exclude: ['all', 'key', 'property'])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} validation failed', diff --git a/src/Validators/Length.php b/src/Validators/Length.php index b8677eba..0f594c72 100644 --- a/src/Validators/Length.php +++ b/src/Validators/Length.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; use Countable as PhpCountable; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -29,6 +30,7 @@ use function is_array; use function is_string; use function mb_strlen; +#[Mixin(prefix: 'length', requireInclusion: 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 326effe8..afe61bfa 100644 --- a/src/Validators/LessThan.php +++ b/src/Validators/LessThan.php @@ -15,9 +15,11 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; +#[Mixin(include: ['length', 'max', 'min'])] #[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 15aa78f5..1d50b4a7 100644 --- a/src/Validators/LessThanOrEqual.php +++ b/src/Validators/LessThanOrEqual.php @@ -14,9 +14,11 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; +#[Mixin(include: ['length', 'max', 'min'])] #[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 eafec0a4..6f1ed353 100644 --- a/src/Validators/Max.php +++ b/src/Validators/Max.php @@ -15,12 +15,14 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use function max; +#[Mixin(prefix: 'max', requireInclusion: 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 1d453010..f246e86d 100644 --- a/src/Validators/Min.php +++ b/src/Validators/Min.php @@ -15,12 +15,14 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use function min; +#[Mixin(prefix: 'min', requireInclusion: 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 193d1b79..c7986ad7 100644 --- a/src/Validators/Multiple.php +++ b/src/Validators/Multiple.php @@ -17,10 +17,12 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; +#[Mixin(include: ['length', 'max', 'min'])] #[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 85534f94..bf2f2d34 100644 --- a/src/Validators/Named.php +++ b/src/Validators/Named.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Name; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -19,6 +20,7 @@ use Respect\Validation\Validators\Core\Nameable; use function is_string; +#[Mixin(exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] #[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 0e4f6ccc..84ff7177 100644 --- a/src/Validators/Not.php +++ b/src/Validators/Not.php @@ -18,9 +18,11 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Result; use Respect\Validation\Validator; +#[Mixin(prefix: 'not', exclude: ['not'])] #[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 d977a94e..24584ce3 100644 --- a/src/Validators/NullOr.php +++ b/src/Validators/NullOr.php @@ -14,12 +14,14 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; 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'])] #[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 caa029d7..6d6735c9 100644 --- a/src/Validators/Odd.php +++ b/src/Validators/Odd.php @@ -17,6 +17,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ use function is_numeric; use const FILTER_VALIDATE_INT; +#[Mixin(include: ['length', 'max', 'min'])] #[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 89799853..ddc687dc 100644 --- a/src/Validators/Positive.php +++ b/src/Validators/Positive.php @@ -15,11 +15,13 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_numeric; +#[Mixin(include: ['length', 'max', 'min'])] #[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 b7eebe12..58d3d79a 100644 --- a/src/Validators/Property.php +++ b/src/Validators/Property.php @@ -19,10 +19,12 @@ namespace Respect\Validation\Validators; use Attribute; use ReflectionClass; use ReflectionObject; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validator; +#[Mixin(prefix: 'property', prefixParameter: true, exclude: ['all', 'key', 'property'])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Property implements Validator { diff --git a/src/Validators/PropertyExists.php b/src/Validators/PropertyExists.php index a6f82daf..856ea0f8 100644 --- a/src/Validators/PropertyExists.php +++ b/src/Validators/PropertyExists.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; use ReflectionClass; use ReflectionObject; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; @@ -21,6 +22,7 @@ use Respect\Validation\Validator; use function is_object; +#[Mixin(exclude: ['all', 'key', 'property'])] #[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 cc466dfb..932f6cd2 100644 --- a/src/Validators/PropertyOptional.php +++ b/src/Validators/PropertyOptional.php @@ -13,9 +13,11 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Result; use Respect\Validation\Validator; +#[Mixin(exclude: ['all', 'key', 'property'])] #[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 bea88903..d647690a 100644 --- a/src/Validators/Templated.php +++ b/src/Validators/Templated.php @@ -12,9 +12,11 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Result; use Respect\Validation\Validator; +#[Mixin(exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] #[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 eb0741dc..3b004539 100644 --- a/src/Validators/Undef.php +++ b/src/Validators/Undef.php @@ -16,11 +16,13 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; +#[Mixin(exclude: ['nullOr', 'undefOr'])] #[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 48108dc7..6cbfe825 100644 --- a/src/Validators/UndefOr.php +++ b/src/Validators/UndefOr.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Respect\Validation\Validators; use Attribute; +use Respect\Dev\CodeGen\Attributes\Mixin; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -20,6 +21,7 @@ use Respect\Validation\Validator; use function array_map; +#[Mixin(prefix: 'undefOr', exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'or must be undefined',