diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 0000000..8cabf30 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,23 @@ +path = $path; + } +} diff --git a/src/Exception/ConstraintViolation.php b/src/Exception/ValueRuleViolation.php similarity index 64% rename from src/Exception/ConstraintViolation.php rename to src/Exception/ValueRuleViolation.php index ee0a2a5..db4d2e8 100644 --- a/src/Exception/ConstraintViolation.php +++ b/src/Exception/ValueRuleViolation.php @@ -6,6 +6,6 @@ use DomainException; -final class ConstraintViolation extends DomainException +final class ValueRuleViolation extends DomainException { } diff --git a/src/Normalizing/Binary.php b/src/Normalizer/Binary.php similarity index 82% rename from src/Normalizing/Binary.php rename to src/Normalizer/Binary.php index fa1cedc..714124f 100644 --- a/src/Normalizing/Binary.php +++ b/src/Normalizer/Binary.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Norvica\Validation\Normalizing; +namespace Norvica\Validation\Normalizer; final class Binary { diff --git a/src/Normalizing/Lower.php b/src/Normalizer/Lower.php similarity index 86% rename from src/Normalizing/Lower.php rename to src/Normalizer/Lower.php index cf5e88e..450d0bd 100644 --- a/src/Normalizing/Lower.php +++ b/src/Normalizer/Lower.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Norvica\Validation\Normalizing; +namespace Norvica\Validation\Normalizer; final class Lower { diff --git a/src/Normalizing/Normalizable.php b/src/Normalizer/Normalizable.php similarity index 78% rename from src/Normalizing/Normalizable.php rename to src/Normalizer/Normalizable.php index 5973a59..eff42f6 100644 --- a/src/Normalizing/Normalizable.php +++ b/src/Normalizer/Normalizable.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Norvica\Validation\Normalizing; +namespace Norvica\Validation\Normalizer; interface Normalizable { diff --git a/src/Normalizing/Numeric.php b/src/Normalizer/Numeric.php similarity index 79% rename from src/Normalizing/Numeric.php rename to src/Normalizer/Numeric.php index 1bc1609..e44ceff 100644 --- a/src/Normalizing/Numeric.php +++ b/src/Normalizer/Numeric.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Norvica\Validation\Normalizing; +namespace Norvica\Validation\Normalizer; final class Numeric { diff --git a/src/Normalizing/Spaceless.php b/src/Normalizer/Spaceless.php similarity index 90% rename from src/Normalizing/Spaceless.php rename to src/Normalizer/Spaceless.php index bd6c99d..08fcd03 100644 --- a/src/Normalizing/Spaceless.php +++ b/src/Normalizer/Spaceless.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Norvica\Validation\Normalizing; +namespace Norvica\Validation\Normalizer; final class Spaceless { diff --git a/src/Normalizing/Trim.php b/src/Normalizer/Trim.php similarity index 90% rename from src/Normalizing/Trim.php rename to src/Normalizer/Trim.php index 8c13244..e20b91c 100644 --- a/src/Normalizing/Trim.php +++ b/src/Normalizer/Trim.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Norvica\Validation\Normalizing; +namespace Norvica\Validation\Normalizer; final readonly class Trim { diff --git a/src/Normalizing/Upper.php b/src/Normalizer/Upper.php similarity index 86% rename from src/Normalizing/Upper.php rename to src/Normalizer/Upper.php index 01e050c..faa7eb3 100644 --- a/src/Normalizing/Upper.php +++ b/src/Normalizer/Upper.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Norvica\Validation\Normalizing; +namespace Norvica\Validation\Normalizer; final class Upper { diff --git a/src/Constraint/Email.php b/src/Rule/Email.php similarity index 66% rename from src/Constraint/Email.php rename to src/Rule/Email.php index 13e938c..08b0201 100644 --- a/src/Constraint/Email.php +++ b/src/Rule/Email.php @@ -2,15 +2,17 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Lower; -use Norvica\Validation\Normalizing\Normalizable; -use Norvica\Validation\Normalizing\Trim; +use Attribute; +use Norvica\Validation\Normalizer\Lower; +use Norvica\Validation\Normalizer\Normalizable; +use Norvica\Validation\Normalizer\Trim; use Norvica\Validation\Validation\EmailValidation; use Override; -readonly class Email implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Email implements Rule, Normalizable { /** * @param bool $dns Perform DNS checks (default `false`). diff --git a/src/Constraint/Flag.php b/src/Rule/Flag.php similarity index 64% rename from src/Constraint/Flag.php rename to src/Rule/Flag.php index de49f0e..865fd76 100644 --- a/src/Constraint/Flag.php +++ b/src/Rule/Flag.php @@ -2,14 +2,16 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Binary; -use Norvica\Validation\Normalizing\Normalizable; +use Attribute; +use Norvica\Validation\Normalizer\Binary; +use Norvica\Validation\Normalizer\Normalizable; use Norvica\Validation\Validation\FlagValidation; use Override; -readonly class Flag implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Flag implements Rule, Normalizable { public function __construct( public bool $value, diff --git a/src/Constraint/Iban.php b/src/Rule/Iban.php similarity index 57% rename from src/Constraint/Iban.php rename to src/Rule/Iban.php index c7b6410..792b639 100644 --- a/src/Constraint/Iban.php +++ b/src/Rule/Iban.php @@ -2,15 +2,17 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Normalizable; -use Norvica\Validation\Normalizing\Spaceless; -use Norvica\Validation\Normalizing\Upper; +use Attribute; +use Norvica\Validation\Normalizer\Normalizable; +use Norvica\Validation\Normalizer\Spaceless; +use Norvica\Validation\Normalizer\Upper; use Norvica\Validation\Validation\IbanValidation; use Override; -readonly class Iban implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Iban implements Rule, Normalizable { #[Override] public static function normalizers(): array diff --git a/src/Constraint/Ip.php b/src/Rule/Ip.php similarity index 65% rename from src/Constraint/Ip.php rename to src/Rule/Ip.php index d410ea3..828c239 100644 --- a/src/Constraint/Ip.php +++ b/src/Rule/Ip.php @@ -2,14 +2,16 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Normalizable; -use Norvica\Validation\Normalizing\Trim; +use Attribute; +use Norvica\Validation\Normalizer\Normalizable; +use Norvica\Validation\Normalizer\Trim; use Norvica\Validation\Validation\IpValidation; use Override; -readonly class Ip implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Ip implements Rule, Normalizable { public function __construct( public int|null $version = null, diff --git a/src/Constraint/Number.php b/src/Rule/Number.php similarity index 67% rename from src/Constraint/Number.php rename to src/Rule/Number.php index 7412678..83e291e 100644 --- a/src/Constraint/Number.php +++ b/src/Rule/Number.php @@ -2,14 +2,16 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Normalizable; -use Norvica\Validation\Normalizing\Numeric; +use Attribute; +use Norvica\Validation\Normalizer\Normalizable; +use Norvica\Validation\Normalizer\Numeric; use Norvica\Validation\Validation\NumberValidation; use Override; -readonly class Number implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Number implements Rule, Normalizable { public function __construct( public int|float|null $min = null, diff --git a/src/Constraint/Option.php b/src/Rule/Option.php similarity index 68% rename from src/Constraint/Option.php rename to src/Rule/Option.php index fa28136..b376729 100644 --- a/src/Constraint/Option.php +++ b/src/Rule/Option.php @@ -2,14 +2,16 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Normalizable; -use Norvica\Validation\Normalizing\Trim; +use Attribute; +use Norvica\Validation\Normalizer\Normalizable; +use Norvica\Validation\Normalizer\Trim; use Norvica\Validation\Validation\OptionValidation; use Override; -readonly class Option implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Option implements Rule, Normalizable { /** * @param string[] $options diff --git a/src/Constraint/Password.php b/src/Rule/Password.php similarity index 84% rename from src/Constraint/Password.php rename to src/Rule/Password.php index 00406b9..a70660a 100644 --- a/src/Constraint/Password.php +++ b/src/Rule/Password.php @@ -2,13 +2,15 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Normalizable; +use Attribute; +use Norvica\Validation\Normalizer\Normalizable; use Norvica\Validation\Validation\PasswordValidation; use Override; -readonly class Password implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Password implements Rule, Normalizable { /** * @param int $min Minimal password length (default `8`). diff --git a/src/Constraint/Validatable.php b/src/Rule/Rule.php similarity index 67% rename from src/Constraint/Validatable.php rename to src/Rule/Rule.php index 17c52b2..6b35fc0 100644 --- a/src/Constraint/Validatable.php +++ b/src/Rule/Rule.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -interface Validatable +interface Rule { /** * @return callable-string diff --git a/src/Constraint/Slug.php b/src/Rule/Slug.php similarity index 76% rename from src/Constraint/Slug.php rename to src/Rule/Slug.php index 4838f9d..aa2e262 100644 --- a/src/Constraint/Slug.php +++ b/src/Rule/Slug.php @@ -2,8 +2,11 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; +use Attribute; + +#[Attribute(Attribute::TARGET_PROPERTY)] readonly class Slug extends Text { public function __construct( diff --git a/src/Constraint/Text.php b/src/Rule/Text.php similarity index 69% rename from src/Constraint/Text.php rename to src/Rule/Text.php index 4e7a7c9..f0b13c7 100644 --- a/src/Constraint/Text.php +++ b/src/Rule/Text.php @@ -2,14 +2,16 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Normalizable; -use Norvica\Validation\Normalizing\Trim; +use Attribute; +use Norvica\Validation\Normalizer\Normalizable; +use Norvica\Validation\Normalizer\Trim; use Norvica\Validation\Validation\TextValidation; use Override; -readonly class Text implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Text implements Rule, Normalizable { public function __construct( public int|null $minLength = null, diff --git a/src/Constraint/Uuid.php b/src/Rule/Uuid.php similarity index 62% rename from src/Constraint/Uuid.php rename to src/Rule/Uuid.php index 37524cd..f2bd6d6 100644 --- a/src/Constraint/Uuid.php +++ b/src/Rule/Uuid.php @@ -2,15 +2,17 @@ declare(strict_types=1); -namespace Norvica\Validation\Constraint; +namespace Norvica\Validation\Rule; -use Norvica\Validation\Normalizing\Lower; -use Norvica\Validation\Normalizing\Normalizable; -use Norvica\Validation\Normalizing\Trim; +use Attribute; +use Norvica\Validation\Normalizer\Lower; +use Norvica\Validation\Normalizer\Normalizable; +use Norvica\Validation\Normalizer\Trim; use Norvica\Validation\Validation\UuidValidation; use Override; -readonly class Uuid implements Validatable, Normalizable +#[Attribute(Attribute::TARGET_PROPERTY)] +readonly class Uuid implements Rule, Normalizable { public function __construct( public int|null $version = null, diff --git a/src/Validation/EmailValidation.php b/src/Validation/EmailValidation.php index f9afd1b..d85f7db 100644 --- a/src/Validation/EmailValidation.php +++ b/src/Validation/EmailValidation.php @@ -4,8 +4,8 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Email; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Email; +use Norvica\Validation\Exception\ValueRuleViolation; final class EmailValidation { @@ -14,18 +14,18 @@ public function __invoke(string $value, Email $constraint): void $message = 'Value must be a valid E-mail address'; if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { - throw new ConstraintViolation($message); + throw new ValueRuleViolation($message); } [$local, $domain] = explode('@', $value, 2); if (!preg_match('/^[a-zA-Z0-9.!$%&\'*+\/=?^_`{|}~-]{1,64}$/', $local)) { - throw new ConstraintViolation($message); + throw new ValueRuleViolation($message); } // DNS check (A/MX records) if ($constraint->dns && !checkdnsrr($domain) && !checkdnsrr($domain, 'A')) { - throw new ConstraintViolation($message); + throw new ValueRuleViolation($message); } } } diff --git a/src/Validation/FlagValidation.php b/src/Validation/FlagValidation.php index da44c84..7a4ed11 100644 --- a/src/Validation/FlagValidation.php +++ b/src/Validation/FlagValidation.php @@ -4,8 +4,8 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Flag; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Flag; +use Norvica\Validation\Exception\ValueRuleViolation; final class FlagValidation { @@ -14,7 +14,7 @@ public function __invoke(bool $value, Flag $constraint): void if ($value !== $constraint->value) { $parameter = $constraint->value ? 'true' : 'false'; - throw new ConstraintViolation("Value must be {$parameter}"); + throw new ValueRuleViolation("Value must be {$parameter}"); } } } diff --git a/src/Validation/IbanValidation.php b/src/Validation/IbanValidation.php index ebb9f98..f8b1623 100644 --- a/src/Validation/IbanValidation.php +++ b/src/Validation/IbanValidation.php @@ -4,8 +4,8 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Iban; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Iban; +use Norvica\Validation\Exception\ValueRuleViolation; /** * @see https://www.iban.com/structure @@ -109,7 +109,7 @@ public function __invoke(string $value, Iban $constraint): void $country = substr($value, 0, 2); $length = self::LENGTH[$country] ?? null; if (strlen($value) !== $length) { - throw new ConstraintViolation($message); + throw new ValueRuleViolation($message); } // move first 4 chars (country code and check digits) to the end @@ -123,7 +123,7 @@ public function __invoke(string $value, Iban $constraint): void $mod = self::mod($value, '97'); if ($mod !== '1') { - throw new ConstraintViolation($message); + throw new ValueRuleViolation($message); } } diff --git a/src/Validation/IpValidation.php b/src/Validation/IpValidation.php index f7b4c05..81a3e0a 100644 --- a/src/Validation/IpValidation.php +++ b/src/Validation/IpValidation.php @@ -4,8 +4,8 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Ip; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Ip; +use Norvica\Validation\Exception\ValueRuleViolation; final class IpValidation { @@ -20,7 +20,7 @@ public function __invoke(string $value, Ip $constraint): void }; if (!$valid) { - throw new ConstraintViolation( + throw new ValueRuleViolation( $constraint->version ? "Value must be a valid IPv{$constraint->version} address" : 'Value must be a valid IP address' diff --git a/src/Validation/NumberValidation.php b/src/Validation/NumberValidation.php index 51abb18..96c3c50 100644 --- a/src/Validation/NumberValidation.php +++ b/src/Validation/NumberValidation.php @@ -4,19 +4,19 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Number; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Number; +use Norvica\Validation\Exception\ValueRuleViolation; final class NumberValidation { public function __invoke(int|float $value, Number $constraint): void { if ($constraint->min !== null && $constraint->min > $value) { - throw new ConstraintViolation("Value must be higher than {$constraint->min}"); + throw new ValueRuleViolation("Value must be higher than {$constraint->min}"); } if ($constraint->max !== null && $constraint->max < $value) { - throw new ConstraintViolation("Value must be lower than {$constraint->min}"); + throw new ValueRuleViolation("Value must be lower than {$constraint->min}"); } } } diff --git a/src/Validation/OptionValidation.php b/src/Validation/OptionValidation.php index b8236c4..6ec5b38 100644 --- a/src/Validation/OptionValidation.php +++ b/src/Validation/OptionValidation.php @@ -4,8 +4,8 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Option; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Option; +use Norvica\Validation\Exception\ValueRuleViolation; final class OptionValidation { @@ -13,16 +13,16 @@ public function __invoke(array|string|int|float $value, Option $constraint): voi { if (is_array($value)) { if (!$constraint->multiple) { - throw new ConstraintViolation('Multiple options are not allowed'); + throw new ValueRuleViolation('Multiple options are not allowed'); } if (!array_is_list($value)) { - throw new ConstraintViolation('Options must be a numerically indexed array'); + throw new ValueRuleViolation('Options must be a numerically indexed array'); } $diff = array_diff($value, $constraint->options); if (count($diff) > 0) { - throw new ConstraintViolation( + throw new ValueRuleViolation( sprintf('Values must match allowed options: %s', implode('", "', $constraint->options)) ); } @@ -31,7 +31,7 @@ public function __invoke(array|string|int|float $value, Option $constraint): voi } if (!in_array($value, $constraint->options, true)) { - throw new ConstraintViolation( + throw new ValueRuleViolation( sprintf('Value must match one of the allowed options: %s', implode('", "', $constraint->options)) ); } diff --git a/src/Validation/PasswordValidation.php b/src/Validation/PasswordValidation.php index 510f0e9..125e390 100644 --- a/src/Validation/PasswordValidation.php +++ b/src/Validation/PasswordValidation.php @@ -4,8 +4,8 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Password; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Password; +use Norvica\Validation\Exception\ValueRuleViolation; final class PasswordValidation { @@ -14,11 +14,11 @@ public function __invoke(string $value, Password $constraint): void $length = strlen($value); if ($length > 128) { - throw new ConstraintViolation("Password must not be longer than 128 characters"); + throw new ValueRuleViolation("Password must not be longer than 128 characters"); } if ($length < $constraint->min) { - throw new ConstraintViolation("Password must be at least {$constraint->min} characters long"); + throw new ValueRuleViolation("Password must be at least {$constraint->min} characters long"); } $hasUpper = preg_match('#[A-Z]#', $value); @@ -27,19 +27,19 @@ public function __invoke(string $value, Password $constraint): void $hasSpecial = preg_match('#[^A-Za-z0-9]#', $value); if ($constraint->upper && !$hasUpper) { - throw new ConstraintViolation("Password must contain at least 1 upper case character"); + throw new ValueRuleViolation("Password must contain at least 1 upper case character"); } if ($constraint->lower && !$hasLower) { - throw new ConstraintViolation("Password must contain at least 1 lower case character"); + throw new ValueRuleViolation("Password must contain at least 1 lower case character"); } if ($constraint->number && !$hasNumber) { - throw new ConstraintViolation("Password must contain at least 1 number"); + throw new ValueRuleViolation("Password must contain at least 1 number"); } if ($constraint->special && !$hasSpecial) { - throw new ConstraintViolation("Password must contain at least 1 special character"); + throw new ValueRuleViolation("Password must contain at least 1 special character"); } } } diff --git a/src/Validation/TextValidation.php b/src/Validation/TextValidation.php index abff17d..50c6e78 100644 --- a/src/Validation/TextValidation.php +++ b/src/Validation/TextValidation.php @@ -4,8 +4,8 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Text; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Text; +use Norvica\Validation\Exception\ValueRuleViolation; final class TextValidation { @@ -14,15 +14,15 @@ public function __invoke(string $value, Text $constraint): void $length = strlen($value); if ($constraint->minLength !== null && $constraint->minLength > $length) { - throw new ConstraintViolation("Value must be at least {$constraint->minLength} characters long"); + throw new ValueRuleViolation("Value must be at least {$constraint->minLength} characters long"); } if ($constraint->maxLength !== null && $constraint->maxLength < $length) { - throw new ConstraintViolation("Value must be no more than {$constraint->maxLength} characters long"); + throw new ValueRuleViolation("Value must be no more than {$constraint->maxLength} characters long"); } if (null !== $constraint->regExp && !preg_match($constraint->regExp, $value)) { - throw new ConstraintViolation("Value doesn't match the required format"); + throw new ValueRuleViolation("Value doesn't match the required format"); } } } diff --git a/src/Validation/UuidValidation.php b/src/Validation/UuidValidation.php index e39e856..d909613 100644 --- a/src/Validation/UuidValidation.php +++ b/src/Validation/UuidValidation.php @@ -4,8 +4,8 @@ namespace Norvica\Validation\Validation; -use Norvica\Validation\Constraint\Uuid; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Uuid; +use Norvica\Validation\Exception\ValueRuleViolation; final class UuidValidation { @@ -13,7 +13,7 @@ public function __invoke(string $uuid, Uuid $constraint): void { if ($constraint->version === null) { if (!preg_match('#^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$#i', $uuid)) { - throw new ConstraintViolation('Value must be a valid UUID'); + throw new ValueRuleViolation('Value must be a valid UUID'); } return; @@ -21,7 +21,7 @@ public function __invoke(string $uuid, Uuid $constraint): void $pattern = '#^[0-9A-F]{8}-[0-9A-F]{4}-' . $constraint->version . '[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$#i'; if (!preg_match($pattern, $uuid)) { - throw new ConstraintViolation("Value must be a valid UUID (version {$constraint->version})"); + throw new ValueRuleViolation("Value must be a valid UUID (version {$constraint->version})"); } } } diff --git a/src/Validator.php b/src/Validator.php index 80a6a8c..5219b53 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -4,32 +4,141 @@ namespace Norvica\Validation; -use Norvica\Validation\Constraint\Validatable; -use Norvica\Validation\Exception\ConstraintViolation; -use Norvica\Validation\Normalizing\Normalizable; -use TypeError; +use Norvica\Validation\Exception\LogicException; +use Norvica\Validation\Exception\PropertyRuleViolation; +use Norvica\Validation\Rule\Rule; +use Norvica\Validation\Exception\ValueRuleViolation; +use Norvica\Validation\Normalizer\Normalizable; +use ReflectionClass; +use stdClass; +use UnexpectedValueException; final class Validator { /** - * @throws ConstraintViolation + * @throws ValueRuleViolation */ - public function check(mixed $value, Validatable|Normalizable $constraint): void + public function validate(mixed $value, Rule|array|null $rules = null, bool $strict = true): void { - if ($constraint instanceof Normalizable) { - foreach ($constraint->normalizers() as $sanitize) { - $value = $sanitize($value); + $this->traverse([], $strict, $value, $rules); + } + + /** + * @param string[] $path + * @throws ValueRuleViolation + */ + private function traverse(array $path, bool $strict, mixed $value, Rule|array|null $rules = null): void + { + // scalar or list + if ($value === null || is_scalar($value) || (is_array($value) && array_is_list($value))) { + $this->single($path, $strict, $value, $rules); + + return; + } + + // associative array + if (is_array($value)) { + $this->array($path, $strict, $value, $rules); + + return; + } + + // object + if (is_object($value)) { + $this->object($path, $strict, $value, $rules); + + return; + } + + throw new UnexpectedValueException(sprintf('Value of type %s cannot be validated.', get_debug_type($value))); + } + + /** + * @param string[] $path + * @param array $rules + */ + private function array(array $path, bool $strict, array $values, array|null $rules = null): void + { + $rules = $rules ?: []; + + foreach ($values as $key => $value) { + $this->traverse([...$path, $key], $strict, $value, $rules[$key] ?? null); + } + } + + /** + * @param string[] $path + * @param array|null $rules + * @throws ValueRuleViolation + */ + private function object(array $path, bool $strict, object $value, array|null $rules = null): void + { + $rules = $rules ?: []; + + if ($value instanceof stdClass) { + $this->array($path, $strict, (array) $value, $rules); + + return; + } + + $rc = new ReflectionClass($value); + foreach ($rc->getProperties() as $rp) { + $key = $rp->getName(); + $rule = $rules[$key] ?? null; + if ($rule === null) { + $attributes = $rp->getAttributes(); + foreach ($attributes as $attribute) { + $instance = $attribute->newInstance(); + if (!$instance instanceof Rule) { + continue; + } + + // TODO: support for multiple rules + $rule = $instance; + + break; + } } + + $this->traverse([...$path, $key], $strict, $rp->getValue($value), $rule); } + } - if (!$constraint instanceof Validatable) { - throw new TypeError( - sprintf("Constraint '%s' must implement '%s' interface.", get_debug_type($constraint), Validatable::class) - ); + /** + * @param string[] $path + * @throws ValueRuleViolation + */ + private function single(array $path, bool $strict, array|string|int|float|bool|null $value, Rule|null $rule = null): void + { + if ($rule === null) { + if ($strict) { + throw new LogicException( + message: 'Validation rule is not configured.', + path: $path, + ); + } + + return; + } + + if ($rule instanceof Normalizable) { + foreach ($rule->normalizers() as $sanitize) { + $value = $sanitize($value); + } } - $class = $constraint->validator(); + $class = $rule->validator(); $validator = new $class(); // TODO: allow PSR container integration - $validator($value, $constraint); + + try { + $validator($value, $rule); + } catch (ValueRuleViolation $e) { + throw new PropertyRuleViolation( + message: $e->getMessage(), + code: $e->getCode(), + previous: $e, + path: $path, + ); + } } } diff --git a/tests/Array/ArrayTest.php b/tests/Array/ArrayTest.php new file mode 100644 index 0000000..6e6a8a9 --- /dev/null +++ b/tests/Array/ArrayTest.php @@ -0,0 +1,61 @@ + [ + ['email' => 'john.doe@example.com', 'password' => 'Ul1!oooo'], + ['email' => new Email(), 'password' => new Password()], + ]; + + yield 'nested' => [ + ['allowed' => ['localhost' => '127.0.0.1']], + ['allowed' => ['localhost' => new Ip()]], + ]; + } + + #[DataProvider('valid')] + public function testValid(array $values, array $rules): void + { + $this->assertValid($values, $rules); + } + + public static function invalid(): Generator + { + yield 'login (invalid e-mail)' => [ + ['email' => 'john.doe@', 'password' => 'Ul1!oooo'], + ['email' => new Email(), 'password' => new Password()], + ['email'], + ]; + + yield 'login (invalid password)' => [ + ['email' => 'john.doe@example.com', 'password' => 'oooo'], + ['email' => new Email(), 'password' => new Password()], + ['password'], + ]; + + yield 'nested' => [ + ['allowed' => ['localhost' => '0.0.0']], + ['allowed' => ['localhost' => new Ip()]], + ['allowed', 'localhost'], + ]; + } + + #[DataProvider('invalid')] + public function testInvalid(array $values, array $rules, array $path): void + { + $this->assertInvalid($values, $rules, $path); + } +} diff --git a/tests/Object/ObjectTest.php b/tests/Object/ObjectTest.php new file mode 100644 index 0000000..f834d06 --- /dev/null +++ b/tests/Object/ObjectTest.php @@ -0,0 +1,172 @@ + [ + (object) ['email' => 'john.doe@example.com', 'password' => 'Ul1!oooo'], + ['email' => new Email(), 'password' => new Password()], + ]; + + yield 'anonymous class with array rules' => [ + new class ('john.doe@example.com', 'Ul1!oooo') { + public function __construct( + public string $email, + public string $password, + ) { + } + }, + ['email' => new Email(), 'password' => new Password()], + ]; + + yield 'anonymous class with attribute rules' => [ + new class ('john.doe@example.com', 'Ul1!oooo') { + public function __construct( + #[Email] + public string $email, + #[Password] + public string $password, + ) { + } + }, + null, + ]; + + yield 'anonymous class with nested class attribute rules' => [ + new class ( + new class ('0.0.0.0') { + public function __construct( + #[Ip] + public string $localhost, + ) { + } + }, + ) { + public function __construct( + public object $allowed, + ) { + } + }, + null, + ]; + + yield 'anonymous class with nested class array rules' => [ + new class ( + new class ('0.0.0.0') { + public function __construct( + public string $localhost, + ) { + } + }, + ) { + public function __construct( + public object $allowed, + ) { + } + }, + ['allowed' => ['localhost' => new Ip()]], + ]; + } + + #[DataProvider('valid')] + public function testValid(object $value, array|null $rules): void + { + $this->assertValid($value, $rules); + } + + public static function invalid(): Generator + { + yield 'stdClass' => [ + (object) ['email' => 'john.doe@example.com', 'password' => 'oooo'], + ['email' => new Email(), 'password' => new Password()], + ['password'], + ]; + + yield 'stdClass (nested)' => [ + (object) ['allowed' => ['localhost' => '0.0.0']], + ['allowed' => ['localhost' => new Ip()]], + ['allowed', 'localhost'], + ]; + + yield 'anonymous class with array rules' => [ + new class ('john.doe@example.com', 'oooo') { + public function __construct( + public string $email, + public string $password, + ) { + } + }, + ['email' => new Email(), 'password' => new Password()], + ['password'], + ]; + + yield 'anonymous class with attribute rules' => [ + new class ('john.doe@example.com', 'oooo') { + public function __construct( + #[Email] + public string $email, + #[Password] + public string $password, + ) { + } + }, + null, + ['password'], + ]; + + yield 'anonymous class with nested class attribute rules' => [ + new class ( + new class ('0.0.0.') { + public function __construct( + #[Ip] + public string $localhost, + ) { + } + }, + ) { + public function __construct( + public object $allowed, + ) { + } + }, + null, + ['allowed', 'localhost'], + ]; + + yield 'anonymous class with nested class array rules' => [ + new class ( + new class ('0.0.0.') { + public function __construct( + public string $localhost, + ) { + } + }, + ) { + public function __construct( + public object $allowed, + ) { + } + }, + ['allowed' => ['localhost' => new Ip()]], + ['allowed', 'localhost'], + ]; + } + + #[DataProvider('invalid')] + public function testInvalid(object $value, array|null $rules, array $path): void + { + $this->assertInvalid($value, $rules, $path); + } +} diff --git a/tests/Validation/EmailTest.php b/tests/Single/EmailTest.php similarity index 96% rename from tests/Validation/EmailTest.php rename to tests/Single/EmailTest.php index ab66ac6..55e8de2 100644 --- a/tests/Validation/EmailTest.php +++ b/tests/Single/EmailTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Email; +use Norvica\Validation\Rule\Email; use PHPUnit\Framework\Attributes\DataProvider; final class EmailTest extends ValidationTestCase diff --git a/tests/Validation/FlagTest.php b/tests/Single/FlagTest.php similarity index 93% rename from tests/Validation/FlagTest.php rename to tests/Single/FlagTest.php index 30a77a8..1d340c9 100644 --- a/tests/Validation/FlagTest.php +++ b/tests/Single/FlagTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Flag; +use Norvica\Validation\Rule\Flag; use PHPUnit\Framework\Attributes\DataProvider; final class FlagTest extends ValidationTestCase diff --git a/tests/Validation/IbanTest.php b/tests/Single/IbanTest.php similarity index 98% rename from tests/Validation/IbanTest.php rename to tests/Single/IbanTest.php index 1e5fad1..f59ee30 100644 --- a/tests/Validation/IbanTest.php +++ b/tests/Single/IbanTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Iban; +use Norvica\Validation\Rule\Iban; use PHPUnit\Framework\Attributes\DataProvider; /** diff --git a/tests/Validation/IpTest.php b/tests/Single/IpTest.php similarity index 94% rename from tests/Validation/IpTest.php rename to tests/Single/IpTest.php index 7ff6df9..166ae34 100644 --- a/tests/Validation/IpTest.php +++ b/tests/Single/IpTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Ip; -use Norvica\Validation\Exception\ConstraintViolation; +use Norvica\Validation\Rule\Ip; +use Norvica\Validation\Exception\ValueRuleViolation; use Norvica\Validation\Validation\IpValidation; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Validation/NumberTest.php b/tests/Single/NumberTest.php similarity index 93% rename from tests/Validation/NumberTest.php rename to tests/Single/NumberTest.php index 84c9c5a..cc0d89b 100644 --- a/tests/Validation/NumberTest.php +++ b/tests/Single/NumberTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Number; +use Norvica\Validation\Rule\Number; use PHPUnit\Framework\Attributes\DataProvider; final class NumberTest extends ValidationTestCase diff --git a/tests/Validation/OptionTest.php b/tests/Single/OptionTest.php similarity index 94% rename from tests/Validation/OptionTest.php rename to tests/Single/OptionTest.php index 62bd0e6..801ab27 100644 --- a/tests/Validation/OptionTest.php +++ b/tests/Single/OptionTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Option; +use Norvica\Validation\Rule\Option; use PHPUnit\Framework\Attributes\DataProvider; final class OptionTest extends ValidationTestCase diff --git a/tests/Validation/PasswordTest.php b/tests/Single/PasswordTest.php similarity index 91% rename from tests/Validation/PasswordTest.php rename to tests/Single/PasswordTest.php index 7185837..707fcc2 100644 --- a/tests/Validation/PasswordTest.php +++ b/tests/Single/PasswordTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Password; +use Norvica\Validation\Rule\Password; use PHPUnit\Framework\Attributes\DataProvider; final class PasswordTest extends ValidationTestCase diff --git a/tests/Validation/SlugTest.php b/tests/Single/SlugTest.php similarity index 90% rename from tests/Validation/SlugTest.php rename to tests/Single/SlugTest.php index f376fc7..4facc47 100644 --- a/tests/Validation/SlugTest.php +++ b/tests/Single/SlugTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Slug; +use Norvica\Validation\Rule\Slug; use PHPUnit\Framework\Attributes\DataProvider; final class SlugTest extends ValidationTestCase diff --git a/tests/Validation/TextTest.php b/tests/Single/TextTest.php similarity index 92% rename from tests/Validation/TextTest.php rename to tests/Single/TextTest.php index 6c1315c..50972af 100644 --- a/tests/Validation/TextTest.php +++ b/tests/Single/TextTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Text; +use Norvica\Validation\Rule\Text; use PHPUnit\Framework\Attributes\DataProvider; final class TextTest extends ValidationTestCase diff --git a/tests/Validation/UuidTest.php b/tests/Single/UuidTest.php similarity index 95% rename from tests/Validation/UuidTest.php rename to tests/Single/UuidTest.php index 138a926..f8cd900 100644 --- a/tests/Validation/UuidTest.php +++ b/tests/Single/UuidTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Tests\Norvica\Validation\Validation; +namespace Tests\Norvica\Validation\Single; use Generator; -use Norvica\Validation\Constraint\Uuid; +use Norvica\Validation\Rule\Uuid; use PHPUnit\Framework\Attributes\DataProvider; final class UuidTest extends ValidationTestCase diff --git a/tests/Single/ValidationTestCase.php b/tests/Single/ValidationTestCase.php new file mode 100644 index 0000000..d9c0ff9 --- /dev/null +++ b/tests/Single/ValidationTestCase.php @@ -0,0 +1,51 @@ +validator = new Validator(); + } + + public function assertValid(mixed $value, Rule|array|null $rules): void + { + try { + $this->validator->validate($value, $rules); + } catch (PropertyRuleViolation $violation) { + $this->fail("Failed asserting value '{$value}' passed validation. {$violation->getMessage()}."); + } + + $this->assertTrue(true); + } + + public function assertInvalid(mixed $value, Rule|array|null $rules, array $path = []): void + { + try { + $this->validator->validate($value, $rules); + } catch (PropertyRuleViolation $violation) { + $this->assertEquals($path, $violation->path); + + return; + } + + $this->fail( + sprintf( + "Failed asserting that exception of type '%s' has been thrown.", + PropertyRuleViolation::class, + ) + ); + } +} diff --git a/tests/Validation/ValidationTestCase.php b/tests/Validation/ValidationTestCase.php deleted file mode 100644 index fb1925e..0000000 --- a/tests/Validation/ValidationTestCase.php +++ /dev/null @@ -1,39 +0,0 @@ -validator = new Validator(); - } - - public function assertValid(mixed $value, Validatable $constraint): void - { - try { - $this->validator->check($value, $constraint); - } catch (ConstraintViolation $violation) { - $this->fail("Failed asserting value '{$value}' passed validation. {$violation->getMessage()}."); - } - - $this->assertTrue(true); - } - - public function assertInvalid(mixed $value, Validatable $constraint): void - { - $this->expectException(ConstraintViolation::class); - - $this->validator->check($value, $constraint); - } -}