mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW Wire up symfony/validator (#11123)
This commit is contained in:
parent
4e658e03aa
commit
7f71695335
@ -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": "*",
|
||||
|
46
src/Core/Validation/ConstraintValidator.php
Normal file
46
src/Core/Validation/ConstraintValidator.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Validation;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use Symfony\Component\Validator\Validation;
|
||||
|
||||
/**
|
||||
* Helper class to abstract away wiring up symfony/validator and getting ValidationResult from validating
|
||||
* symfony validator constraints.
|
||||
*/
|
||||
class ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* Validate a value by a constraint
|
||||
*
|
||||
* @param Constraint|Constraint[] $constraints a constraint or array of constraints to validate against
|
||||
* @param string $fieldName The field name the value relates to, if relevant
|
||||
*/
|
||||
public static function validate(mixed $value, Constraint|array $constraints, string $fieldName = ''): ValidationResult
|
||||
{
|
||||
if (is_array($constraints) && empty($constraints)) {
|
||||
throw new InvalidArgumentException('$constraints must not be an empty array');
|
||||
}
|
||||
|
||||
// Perform validation
|
||||
$validator = Validation::createValidator();
|
||||
$violations = $validator->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;
|
||||
}
|
||||
}
|
366
tests/php/Core/Validation/ConstraintValidatorTest.php
Normal file
366
tests/php/Core/Validation/ConstraintValidatorTest.php
Normal file
@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Tests\Validation;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\All;
|
||||
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
|
||||
use Symfony\Component\Validator\Constraints\Blank;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Constraints\CardScheme;
|
||||
use Symfony\Component\Validator\Constraints\Choice;
|
||||
use Symfony\Component\Validator\Constraints\Cidr;
|
||||
use Symfony\Component\Validator\Constraints\Collection;
|
||||
use Symfony\Component\Validator\Constraints\Count;
|
||||
use Symfony\Component\Validator\Constraints\CssColor;
|
||||
use Symfony\Component\Validator\Constraints\Date;
|
||||
use Symfony\Component\Validator\Constraints\DateTime;
|
||||
use Symfony\Component\Validator\Constraints\DivisibleBy;
|
||||
use Symfony\Component\Validator\Constraints\Email;
|
||||
use Symfony\Component\Validator\Constraints\EqualTo;
|
||||
use Symfony\Component\Validator\Constraints\File;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThan;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\Hostname;
|
||||
use Symfony\Component\Validator\Constraints\Iban;
|
||||
use Symfony\Component\Validator\Constraints\IdenticalTo;
|
||||
use Symfony\Component\Validator\Constraints\Image;
|
||||
use Symfony\Component\Validator\Constraints\Ip;
|
||||
use Symfony\Component\Validator\Constraints\Isbn;
|
||||
use Symfony\Component\Validator\Constraints\IsFalse;
|
||||
use Symfony\Component\Validator\Constraints\Isin;
|
||||
use Symfony\Component\Validator\Constraints\IsNull;
|
||||
use Symfony\Component\Validator\Constraints\Issn;
|
||||
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||
use Symfony\Component\Validator\Constraints\Json;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Constraints\LessThan;
|
||||
use Symfony\Component\Validator\Constraints\LessThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\Luhn;
|
||||
use Symfony\Component\Validator\Constraints\Negative;
|
||||
use Symfony\Component\Validator\Constraints\NegativeOrZero;
|
||||
use Symfony\Component\Validator\Constraints\NoSuspiciousCharacters;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotEqualTo;
|
||||
use Symfony\Component\Validator\Constraints\NotIdenticalTo;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
use Symfony\Component\Validator\Constraints\PasswordStrength;
|
||||
use Symfony\Component\Validator\Constraints\Positive;
|
||||
use Symfony\Component\Validator\Constraints\PositiveOrZero;
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
use Symfony\Component\Validator\Constraints\Regex;
|
||||
use Symfony\Component\Validator\Constraints\Sequentially;
|
||||
use Symfony\Component\Validator\Constraints\Time;
|
||||
use Symfony\Component\Validator\Constraints\Timezone;
|
||||
use Symfony\Component\Validator\Constraints\Type;
|
||||
use Symfony\Component\Validator\Constraints\Ulid;
|
||||
use Symfony\Component\Validator\Constraints\Unique;
|
||||
use Symfony\Component\Validator\Constraints\Url;
|
||||
use Symfony\Component\Validator\Constraints\Uuid;
|
||||
|
||||
class ConstraintValidatorTest extends SapphireTest
|
||||
{
|
||||
protected $usesDatabase = false;
|
||||
|
||||
public function provideValidate(): array
|
||||
{
|
||||
$scenarios = [
|
||||
// basic
|
||||
'NotBlank' => [
|
||||
'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(),
|
||||
],
|
||||
// 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),
|
||||
],
|
||||
];
|
||||
// These classes don't exist until symfony/validator 6.3
|
||||
if (class_exists(PasswordStrength::class)) {
|
||||
$scenarios['PasswordStrength'] = [
|
||||
'value' => 'password',
|
||||
'constraint' => new PasswordStrength(minScore: PasswordStrength::STRENGTH_VERY_STRONG),
|
||||
];
|
||||
}
|
||||
if (class_exists(NoSuspiciousCharacters::class)) {
|
||||
$scenarios['NoSuspiciousCharacters'] = [
|
||||
'value' => '1234567৪',
|
||||
'constraint' => new NoSuspiciousCharacters(),
|
||||
];
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
public function provideValidateResults(): array
|
||||
{
|
||||
return [
|
||||
'single constraint, no field' => [
|
||||
'value' => 'some value',
|
||||
'constraints' => new Blank(),
|
||||
'fieldName' => '',
|
||||
],
|
||||
'single constraint, with field' => [
|
||||
'value' => 'some value',
|
||||
'constraints' => new Blank(),
|
||||
'fieldName' => 'MyField',
|
||||
],
|
||||
'array, no field' => [
|
||||
'value' => 'some value',
|
||||
'constraints' => [new Blank(), new Date()],
|
||||
'fieldName' => '',
|
||||
],
|
||||
'array, with field' => [
|
||||
'value' => 'some value',
|
||||
'constraints' => [new Blank(), new Date()],
|
||||
'fieldName' => 'MyField',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideValidateResults
|
||||
*/
|
||||
public function testValidateResults(mixed $value, Constraint|array $constraints, string $fieldName): void
|
||||
{
|
||||
$result = ConstraintValidator::validate($value, $constraints, $fieldName);
|
||||
$violations = $result->getMessages();
|
||||
$countViolations = is_array($constraints) ? count($constraints) : 1;
|
||||
|
||||
$this->assertCount($countViolations, $violations);
|
||||
foreach ($violations as $violation) {
|
||||
$this->assertSame($fieldName ?: null, $violation['fieldName']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testValidateNoConstraints(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
ConstraintValidator::validate('some value', []);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user