From 0952ad74e63d57d3fbfb970d333ffd42ca2dac63 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 1 Feb 2024 12:12:59 +1300 Subject: [PATCH] NEW Wire up symfony/validator --- composer.json | 1 + src/Core/Validation/ConstraintValidator.php | 41 +++ .../Validation/ConstraintValidatorTest.php | 315 ++++++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 src/Core/Validation/ConstraintValidator.php create mode 100644 tests/php/Core/Validation/ConstraintValidatorTest.php diff --git a/composer.json b/composer.json index ba9b93f1b0b..64761fe4da4 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "symfony/mailer": "^6.1", "symfony/mime": "^6.1", "symfony/translation": "^6.1", + "symfony/validator": "^6.1", "symfony/yaml": "^6.1", "ext-ctype": "*", "ext-dom": "*", diff --git a/src/Core/Validation/ConstraintValidator.php b/src/Core/Validation/ConstraintValidator.php new file mode 100644 index 00000000000..fd8eebd177b --- /dev/null +++ b/src/Core/Validation/ConstraintValidator.php @@ -0,0 +1,41 @@ +validate($value, $constraints); + + // Convert value to ValidationResult + $result = ValidationResult::create(); + /** @var ConstraintViolationInterface $violation */ + foreach ($violations as $violation) { + if ($fieldName) { + $result->addFieldError($fieldName, $violation->getMessage()); + } else { + $result->addError($violation->getMessage()); + } + } + + return $result; + } +} diff --git a/tests/php/Core/Validation/ConstraintValidatorTest.php b/tests/php/Core/Validation/ConstraintValidatorTest.php new file mode 100644 index 00000000000..2b3ae0e4a40 --- /dev/null +++ b/tests/php/Core/Validation/ConstraintValidatorTest.php @@ -0,0 +1,315 @@ + [ + 'value' => '', + 'constraint' => new NotBlank(), + ], + 'Blank' => [ + 'value' => 'not blank', + 'constraint' => new Blank(), + ], + 'NotNull' => [ + 'value' => null, + 'constraint' => new NotNull(), + ], + 'IsNull' => [ + 'value' => 'not null', + 'constraint' => new IsNull(), + ], + 'IsTrue' => [ + 'value' => false, + 'constraint' => new IsTrue(), + ], + 'IsFalse' => [ + 'value' => true, + 'constraint' => new IsFalse(), + ], + 'Type' => [ + 'value' => 'not that type', + 'constraint' => new Type(Type::class), + ], + // strings + 'Email' => [ + 'value' => 'not an email address', + 'constraint' => new Email(), + ], + 'Length' => [ + 'value' => 'not length of 5', + 'constraint' => new Length(exactly: 5), + ], + 'Url' => [ + 'value' => 'not a valid url', + 'constraint' => new Url(), + ], + 'Regex' => [ + 'value' => 'doesnt match that pattern', + 'constraint' => new Regex('/regex/'), + ], + 'Hostname' => [ + 'value' => 'not a valid hostname', + 'constraint' => new Hostname(), + ], + 'Ip' => [ + 'value' => 'not an IP address', + 'constraint' => new Ip(), + ], + 'Cidr' => [ + 'value' => 'not CIDR notation', + 'constraint' => new Cidr(), + ], + 'Json' => [ + 'value' => 'not a JSON string', + 'constraint' => new Json(), + ], + 'Uuid' => [ + 'value' => 'not a UUID', + 'constraint' => new Uuid(), + ], + 'Ulid' => [ + 'value' => 'not a ULID', + 'constraint' => new Ulid(), + ], + 'CssColor' => [ + 'value' => 'not a color', + 'constraint' => new CssColor(), + ], + 'NoSuspiciousCharacters' => [ + 'value' => '1234567৪', + 'constraint' => new NoSuspiciousCharacters(), + ], + // comparisons + 'EqualTo' => [ + 'value' => 'doesnt match that', + 'constraint' => new EqualTo('match this'), + ], + 'NotEqualTo' => [ + 'value' => 'match this', + 'constraint' => new NotEqualTo('match this'), + ], + 'IdenticalTo' => [ + 'value' => 'not exactly the same', + 'constraint' => new IdenticalTo('exactly the same'), + ], + 'NotIdenticalTo' => [ + 'value' => 'exactly the same', + 'constraint' => new NotIdenticalTo('exactly the same'), + ], + 'LessThan' => [ + 'value' => 35, + 'constraint' => new LessThan(1), + ], + 'LessThanOrEqual' => [ + 'value' => 35, + 'constraint' => new LessThanOrEqual(1), + ], + 'GreaterThan' => [ + 'value' => 1, + 'constraint' => new GreaterThan(35), + ], + 'GreaterThanOrEqual' => [ + 'value' => 1, + 'constraint' => new GreaterThanOrEqual(35), + ], + 'Range' => [ + 'value' => 1, + 'constraint' => new Range(min: 30, max: 35), + ], + 'DivisibleBy' => [ + 'value' => 3, + 'constraint' => new DivisibleBy(2), + ], + 'Unique' => [ + 'value' => ['not unique', 'not unique'], + 'constraint' => new Unique(), + ], + // numbers + 'Positive' => [ + 'value' => -1, + 'constraint' => new Positive(), + ], + 'PositiveOrZero' => [ + 'value' => -1, + 'constraint' => new PositiveOrZero(), + ], + 'Negative' => [ + 'value' => 1, + 'constraint' => new Negative(), + ], + 'NegativeOrZero' => [ + 'value' => 1, + 'constraint' => new NegativeOrZero(), + ], + // dates + 'Date' => [ + 'value' => 'not a date', + 'constraint' => new Date(), + ], + 'DateTime' => [ + 'value' => 'not a datetime', + 'constraint' => new DateTime(), + ], + 'Time' => [ + 'value' => 'not a time', + 'constraint' => new Time(), + ], + 'Timezone' => [ + 'value' => 'not a timezone', + 'constraint' => new Timezone(), + ], + // choices + 'Choice' => [ + 'value' => 'not one of those', + 'constraint' => new Choice(['one', 'of', 'these']), + ], + // files + 'File' => [ + 'value' => 'not a path to a file', + 'constraint' => new File(), + ], + 'Image' => [ + 'value' => 'not a path to an image', + 'constraint' => new Image(), + ], + // fincancial + 'CardScheme' => [ + 'value' => 'not a credit card number', + 'constraint' => new CardScheme(CardScheme::VISA), + ], + 'Luhn' => [ + 'value' => 'not a credit card number', + 'constraint' => new Luhn(), + ], + 'Iban' => [ + 'value' => 'not a valid IBAN', + 'constraint' => new Iban(), + ], + 'Isbn' => [ + 'value' => 'not a valid ISBN', + 'constraint' => new Isbn(), + ], + 'Issn' => [ + 'value' => 'not a valid ISSN', + 'constraint' => new Issn(), + ], + 'Isin' => [ + 'value' => 'not a valid ISIN', + 'constraint' => new Isin(), + ], + // other + 'AtLeastOneOf' => [ + 'value' => 'doesnt match any of the constraints', + 'constraint' => new AtLeastOneOf(constraints: [new Regex('/regex/')]), + ], + 'Sequentially' => [ + 'value' => 'doesnt match the constraints in sequence', + 'constraint' => new Sequentially(constraints: [new Regex('/regex/')]), + ], + 'Callback' => [ + 'value' => 'this value doesnt matter', + 'constraint' => new Callback( + fn ($_, $context) => $context->buildViolation('always fail the validation')->addViolation() + ), + ], + 'All' => [ + 'value' => ['all items passed in fail all of the constraints'], + 'constraint' => new All(constraints: [new Regex('/regex/')]), + ], + 'Collection' => [ + 'value' => ['field1' => 'doesnt match the pattern'], + 'constraint' => new Collection(fields: ['field1' => new Regex('/regex/')]), + ], + 'Count' => [ + 'value' => ['less than 30 items'], + 'constraint' => new Count(min:30), + ], + ]; + // This class doesn't exist until symfony/validator 6.3 + if (class_exists(PasswordStrength::class)) { + $scenarios['PasswordStrength'] = [ + 'value' => 'password', + 'constraint' => new PasswordStrength(minScore: PasswordStrength::STRENGTH_VERY_STRONG), + ]; + } + } + + /** + * Tests that all of the currently supported constraints work without throwing exceptions. + * + * We're not actually testing the validation logic per se - just testing that the validators + * all do some validating (hence why they are all set to fail) without exceptions being thrown. + * + * @dataProvider provideValidate + */ + public function testValidate(mixed $value, Constraint $constraint): void + { + $this->assertFalse(ConstraintValidator::validate($value, $constraint)->isValid()); + } +}