diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index d2eb00fc42c5a..24b3c2b78100b 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * allow to define a reusable set of constraints by extending the `Compound` constraint * added `Sequentially` constraint, to sequentially validate a set of constraints (any violation raised will prevent further validation of the nested constraints) * added the `divisibleBy` option to the `Count` constraint + * added `Uid` constraint and `UidValidator` 5.0.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/Uid.php b/src/Symfony/Component/Validator/Constraints/Uid.php new file mode 100644 index 0000000000000..a6ad792e79874 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Uid.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\InvalidArgumentException; +use Symfony\Component\Validator\Exception\LogicException; + +/** + * @Annotation + */ +class Uid extends Constraint +{ + const INVALID_UID_ERROR = '34fd666d-3eb6-4f82-9965-fa7decd445d0'; + + protected static $errorNames = [ + self::INVALID_UID_ERROR => 'INVALID_UID_ERROR', + ]; + + public const UUID_V1 = 'UUID_V1'; + public const UUID_V3 = 'UUID_V3'; + public const UUID_V4 = 'UUID_V4'; + public const UUID_V5 = 'UUID_V5'; + public const UUID_V6 = 'UUID_V6'; + public const ULID = 'ULID'; + + /** + * @var string[] + * + * @internal + */ + public static $availableTypes = [ + self::UUID_V1, + self::UUID_V3, + self::UUID_V4, + self::UUID_V5, + self::UUID_V6, + self::ULID, + ]; + + public $message = 'This value is not valid.'; + + /** + * @var int[] + */ + public $types = [ + self::UUID_V1, + self::UUID_V3, + self::UUID_V4, + self::UUID_V5, + self::UUID_V6, + self::ULID, + ]; + + public $normalizer; + + public function __construct($options = null) + { + if (!class_exists(AbstractUid::class)) { + throw new LogicException('Unable to use the UID Constraint, as Symfony Uid component is not installed.'); + } + + if (\is_array($options) && \array_key_exists('types', $options)) { + if (!\is_array($options['types'])) { + throw new InvalidArgumentException('The "types" parameter should be an array.'); + } + array_map(function ($value) { + if (!\in_array($value, self::$availableTypes, true)) { + throw new InvalidArgumentException('The "types" parameter is not valid.'); + } + }, $options['types']); + } + + parent::__construct($options); + + if (null !== $this->normalizer && !\is_callable($this->normalizer)) { + throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); + } + } + + public function getDefaultOption() + { + return 'types'; + } +} diff --git a/src/Symfony/Component/Validator/Constraints/UidValidator.php b/src/Symfony/Component/Validator/Constraints/UidValidator.php new file mode 100644 index 0000000000000..c84aafe0a1d1a --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/UidValidator.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Uid\Ulid; +use Symfony\Component\Uid\UuidV1; +use Symfony\Component\Uid\UuidV3; +use Symfony\Component\Uid\UuidV4; +use Symfony\Component\Uid\UuidV5; +use Symfony\Component\Uid\UuidV6; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +class UidValidator extends ConstraintValidator +{ + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) + { + if (!$constraint instanceof Uid) { + throw new UnexpectedTypeException($constraint, Uid::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_string($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = (string) $value; + + foreach ($constraint->types as $type) { + if (Uid::UUID_V1 === $type && UuidV1::isValid($value)) { + return; + } + if (Uid::UUID_V3 === $type && UuidV3::isValid($value)) { + return; + } + if (Uid::UUID_V4 === $type && UuidV4::isValid($value)) { + return; + } + if (Uid::UUID_V5 === $type && UuidV5::isValid($value)) { + return; + } + if (Uid::UUID_V6 === $type && UuidV6::isValid($value)) { + return; + } + if (Uid::ULID === $type && Ulid::isValid($value)) { + return; + } + } + + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(Uid::INVALID_UID_ERROR) + ->addViolation(); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UidTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UidTest.php new file mode 100644 index 0000000000000..df3a368938a00 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/UidTest.php @@ -0,0 +1,44 @@ +expectException('Symfony\Component\Validator\Exception\InvalidArgumentException'); + $this->expectExceptionMessage('The "types" parameter should be an array.'); + $uid = new Uid(['types' => 'foo']); + } + + public function testInvalidTypeTriggerException() + { + $this->expectException('Symfony\Component\Validator\Exception\InvalidArgumentException'); + $this->expectExceptionMessage('The "types" parameter is not valid.'); + $uid = new Uid(['types' => ['foo']]); + } + + public function testNormalizerCanBeSet() + { + $email = new Uid(['normalizer' => 'trim']); + + $this->assertEquals('trim', $email->normalizer); + } + + public function testInvalidNormalizerThrowsException() + { + $this->expectException('Symfony\Component\Validator\Exception\InvalidArgumentException'); + $this->expectExceptionMessage('The "normalizer" option must be a valid callable ("string" given).'); + new Uid(['normalizer' => 'Unknown Callable']); + } + + public function testInvalidNormalizerObjectThrowsException() + { + $this->expectException('Symfony\Component\Validator\Exception\InvalidArgumentException'); + $this->expectExceptionMessage('The "normalizer" option must be a valid callable ("stdClass" given).'); + new Uid(['normalizer' => new \stdClass()]); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UidValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UidValidatorTest.php new file mode 100644 index 0000000000000..3ff9a5c367b9b --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/UidValidatorTest.php @@ -0,0 +1,74 @@ +validator->validate(null, new Uid()); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid() + { + $this->validator->validate('', new Uid()); + + $this->assertNoViolation(); + } + + public function testValidUids() + { + $this->validator->validate('9b7541de-6f87-11ea-ab3c-9da9a81562fc', new Uid()); + $this->validator->validate('e576629b-ff34-3642-9c08-1f5219f0d45b', new Uid()); + $this->validator->validate('4126dbc1-488e-4f6e-aadd-775dcbac482e', new Uid()); + $this->validator->validate('18cdf3d3-ea1b-5b23-a9c5-40abd0e2df22', new Uid()); + $this->validator->validate('1ea6ecef-eb9a-66fe-b62b-957b45f17e43', new Uid()); + $this->validator->validate('01E4BYF64YZ97MDV6RH0HAMN6X', new Uid()); + + $this->assertNoViolation(); + } + + public function testInvalidUid() + { + $value = 'foo'; + + $constraint = new Uid([ + 'message' => 'myMessage', + ]); + + $this->validator->validate($value, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$value.'"') + ->setCode(Uid::INVALID_UID_ERROR) + ->assertRaised(); + } + + public function testInvalidUidForTypes() + { + $value = '9b7541de-6f87-11ea-ab3c-9da9a81562fc'; + + $constraint = new Uid([ + 'message' => 'myMessage', + 'types' => [Uid::UUID_V3], + ]); + + $this->validator->validate($value, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$value.'"') + ->setCode(Uid::INVALID_UID_ERROR) + ->assertRaised(); + } +}