diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 337534645ffe2..b5461ca6a4896 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Make `PasswordStrengthValidator::estimateStrength()` public * Add the `Yaml` constraint for validating YAML content * Add `errorPath` to Unique constraint + * Add the `format` option to the `Ulid` constraint to allow accepting different ULID formats 7.1 --- diff --git a/src/Symfony/Component/Validator/Constraints/Ulid.php b/src/Symfony/Component/Validator/Constraints/Ulid.php index a5e2a47b9face..caf92b022c473 100644 --- a/src/Symfony/Component/Validator/Constraints/Ulid.php +++ b/src/Symfony/Component/Validator/Constraints/Ulid.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Validates that a value is a valid Universally Unique Lexicographically Sortable Identifier (ULID). @@ -35,20 +36,31 @@ class Ulid extends Constraint self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', ]; + public const FORMAT_BASE_32 = 'base32'; + public const FORMAT_BASE_58 = 'base58'; + public string $message = 'This is not a valid ULID.'; + public string $format = self::FORMAT_BASE_32; /** * @param array|null $options * @param string[]|null $groups + * @param self::FORMAT_*|null $format */ public function __construct( ?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null, + ?string $format = null, ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; + $this->format = $format ?? $this->format; + + if (!\in_array($this->format, [self::FORMAT_BASE_32, self::FORMAT_BASE_58], true)) { + throw new ConstraintDefinitionException(sprintf('The "%s" validation format is not supported.', $format)); + } } } diff --git a/src/Symfony/Component/Validator/Constraints/UlidValidator.php b/src/Symfony/Component/Validator/Constraints/UlidValidator.php index 91942721b5c67..e4133d2dc16e4 100644 --- a/src/Symfony/Component/Validator/Constraints/UlidValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UlidValidator.php @@ -40,31 +40,47 @@ public function validate(mixed $value, Constraint $constraint): void $value = (string) $value; - if (26 !== \strlen($value)) { + [$requiredLength, $requiredCharset] = match ($constraint->format) { + Ulid::FORMAT_BASE_32 => [26, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz'], + Ulid::FORMAT_BASE_58 => [22, '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'], + }; + + if ($requiredLength !== \strlen($value)) { $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(26 > \strlen($value) ? Ulid::TOO_SHORT_ERROR : Ulid::TOO_LONG_ERROR) + ->setParameters([ + '{{ value }}' => $this->formatValue($value), + '{{ format }}' => $constraint->format, + ]) + ->setCode($requiredLength > \strlen($value) ? Ulid::TOO_SHORT_ERROR : Ulid::TOO_LONG_ERROR) ->addViolation(); return; } - if (\strlen($value) !== strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) { + if (\strlen($value) !== strspn($value, $requiredCharset)) { $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameters([ + '{{ value }}' => $this->formatValue($value), + '{{ format }}' => $constraint->format, + ]) ->setCode(Ulid::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } - // Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ' - // Cf https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings - if ($value[0] > '7') { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(Ulid::TOO_LARGE_ERROR) - ->addViolation(); + if (Ulid::FORMAT_BASE_32 === $constraint->format) { + // Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ' + // Cf https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings + if ($value[0] > '7') { + $this->context->buildViolation($constraint->message) + ->setParameters([ + '{{ value }}' => $this->formatValue($value), + '{{ format }}' => $constraint->format, + ]) + ->setCode(Ulid::TOO_LARGE_ERROR) + ->addViolation(); + } } } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UlidTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UlidTest.php index 14046e37a0ac5..bb12ef0e90298 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UlidTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UlidTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraints\Ulid; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; @@ -32,6 +33,14 @@ public function testAttributes() self::assertSame(['my_group'], $cConstraint->groups); self::assertSame('some attached data', $cConstraint->payload); } + + public function testUnexpectedValidationFormat() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "invalid" validation format is not supported.'); + + new Ulid(format: 'invalid'); + } } class UlidDummy diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php index abe5490d18520..7d2be16e208d5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php @@ -53,6 +53,13 @@ public function testValidUlid() $this->assertNoViolation(); } + public function testValidUlidAsBase58() + { + $this->validator->validate('1CCD2w4mK2m455S2BAXFht', new Ulid(format: Ulid::FORMAT_BASE_58)); + + $this->assertNoViolation(); + } + /** * @dataProvider getInvalidUlids */ @@ -65,12 +72,15 @@ public function testInvalidUlid(string $ulid, string $code) $this->validator->validate($ulid, $constraint); $this->buildViolation('testMessage') - ->setParameter('{{ value }}', '"'.$ulid.'"') + ->setParameters([ + '{{ value }}' => '"'.$ulid.'"', + '{{ format }}' => Ulid::FORMAT_BASE_32, + ]) ->setCode($code) ->assertRaised(); } - public static function getInvalidUlids() + public static function getInvalidUlids(): array { return [ ['01ARZ3NDEKTSV4RRFFQ69G5FA', Ulid::TOO_SHORT_ERROR], @@ -81,6 +91,34 @@ public static function getInvalidUlids() ]; } + /** + * @dataProvider getInvalidBase58Ulids + */ + public function testInvalidBase58Ulid(string $ulid, string $code) + { + $constraint = new Ulid(message: 'testMessage', format: Ulid::FORMAT_BASE_58); + + $this->validator->validate($ulid, $constraint); + + $this->buildViolation('testMessage') + ->setParameters([ + '{{ value }}' => '"'.$ulid.'"', + '{{ format }}' => Ulid::FORMAT_BASE_58, + ]) + ->setCode($code) + ->assertRaised(); + } + + public static function getInvalidBase58Ulids(): array + { + return [ + ['1CCD2w4mK2m455S2BAXFh', Ulid::TOO_SHORT_ERROR], + ['1CCD2w4mK2m455S2BAXFhttt', Ulid::TOO_LONG_ERROR], + ['1CCD2w4mK2m455S2BAXFhO', Ulid::INVALID_CHARACTERS_ERROR], + ['not-even-ulid-like', Ulid::TOO_SHORT_ERROR], + ]; + } + public function testInvalidUlidNamed() { $constraint = new Ulid(message: 'testMessage'); @@ -88,7 +126,10 @@ public function testInvalidUlidNamed() $this->validator->validate('01ARZ3NDEKTSV4RRFFQ69G5FA', $constraint); $this->buildViolation('testMessage') - ->setParameter('{{ value }}', '"01ARZ3NDEKTSV4RRFFQ69G5FA"') + ->setParameters([ + '{{ value }}' => '"01ARZ3NDEKTSV4RRFFQ69G5FA"', + '{{ format }}' => Ulid::FORMAT_BASE_32, + ]) ->setCode(Ulid::TOO_SHORT_ERROR) ->assertRaised(); }