From 74b2af6b9ad09f74c92fd5c5c073de19b20ae460 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 30 Jun 2025 22:41:44 +0200 Subject: [PATCH 01/28] [Validator] Add SemVer constraint for semantic versioning validation Add a new constraint to validate semantic versioning strings according to the SemVer 2.0.0 specification. Supports partial versions (e.g., "3", "3.1"), full versions (e.g., "3.1.2"), optional "v" prefix, pre-release versions (e.g., "3.1.2-beta"), and build metadata (e.g., "3.1.2+20130313144700"). Configurable options: - requirePrefix: Enforce the "v" prefix - allowPreRelease: Allow pre-release versions (default: true) - allowBuildMetadata: Allow build metadata (default: true) --- .../Validator/Constraints/SemVer.php | 55 +++++ .../Validator/Constraints/SemVerValidator.php | 91 +++++++ .../Tests/Constraints/SemVerValidatorTest.php | 222 ++++++++++++++++++ 3 files changed, 368 insertions(+) create mode 100644 src/Symfony/Component/Validator/Constraints/SemVer.php create mode 100644 src/Symfony/Component/Validator/Constraints/SemVerValidator.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php new file mode 100644 index 0000000000000..55ecb8b2a3550 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -0,0 +1,55 @@ + + * + * 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\Validator\Constraint; + +/** + * Validates that a value is a valid semantic version. + * + * @author Oskar Stark + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class SemVer extends Constraint +{ + public const INVALID_SEMVER_ERROR = '3e7a8b8f-4d8f-4c7a-b5e9-1a2b3c4d5e6f'; + + protected const ERROR_NAMES = [ + self::INVALID_SEMVER_ERROR => 'INVALID_SEMVER_ERROR', + ]; + + public string $message = 'This value is not a valid semantic version.'; + public bool $requirePrefix = false; + public bool $allowPreRelease = true; + public bool $allowBuildMetadata = true; + + /** + * @param array|null $options + * @param string[]|null $groups + */ + public function __construct( + ?array $options = null, + ?string $message = null, + ?bool $requirePrefix = null, + ?bool $allowPreRelease = null, + ?bool $allowBuildMetadata = null, + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->requirePrefix = $requirePrefix ?? $this->requirePrefix; + $this->allowPreRelease = $allowPreRelease ?? $this->allowPreRelease; + $this->allowBuildMetadata = $allowBuildMetadata ?? $this->allowBuildMetadata; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php new file mode 100644 index 0000000000000..4d857eaea3791 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -0,0 +1,91 @@ + + * + * 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\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Oskar Stark + */ +class SemVerValidator extends ConstraintValidator +{ + /** + * Semantic Versioning 2.0.0 regex pattern. + * Supports: 1.0.0, 1.2.3, 1.2.3-alpha, 1.2.3-alpha.1, 1.2.3+20130313144700, 1.2.3-beta+exp.sha.5114f85 + * With optional "v" prefix: v1.0.0, v1.2.3-alpha + */ + private const SEMVER_PATTERN = '/^(?Pv)?(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/'; + + /** + * Loose semantic versioning pattern that allows partial versions. + * Supports: 1, 1.2, 1.2.3, v1, v1.2, v1.2.3, plus all the variations above + */ + private const LOOSE_SEMVER_PATTERN = '/^(?Pv)?(?P0|[1-9]\d*)(?:\.(?P0|[1-9]\d*)(?:\.(?P0|[1-9]\d*))?)?(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/'; + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof SemVer) { + throw new UnexpectedTypeException($constraint, SemVer::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_string($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = (string) $value; + + // Use loose pattern by default to allow partial versions + if (!preg_match(self::LOOSE_SEMVER_PATTERN, $value, $matches)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->addViolation(); + + return; + } + + // Check prefix requirement + if ($constraint->requirePrefix && empty($matches['prefix'])) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->addViolation(); + + return; + } + + // Check pre-release + if (!$constraint->allowPreRelease && !empty($matches['prerelease'])) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->addViolation(); + + return; + } + + // Check build metadata + if (!$constraint->allowBuildMetadata && !empty($matches['buildmetadata'])) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->addViolation(); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php new file mode 100644 index 0000000000000..254315b23b541 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -0,0 +1,222 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\SemVer; +use Symfony\Component\Validator\Constraints\SemVerValidator; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +class SemVerValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): SemVerValidator + { + return new SemVerValidator(); + } + + public function testNullIsValid() + { + $this->validator->validate(null, new SemVer()); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid() + { + $this->validator->validate('', new SemVer()); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidSemVersions + */ + public function testValidSemVersions(string $version) + { + $this->validator->validate($version, new SemVer()); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidSemVersionsWithPrefix + */ + public function testValidSemVersionsWithPrefix(string $version) + { + $this->validator->validate($version, new SemVer(requirePrefix: true)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getInvalidSemVersions + */ + public function testInvalidSemVersions(string $version) + { + $constraint = new SemVer(message: 'myMessage'); + + $this->validator->validate($version, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$version.'"') + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getInvalidSemVersionsWithoutPrefix + */ + public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) + { + $constraint = new SemVer(requirePrefix: true, message: 'myMessage'); + + $this->validator->validate($version, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$version.'"') + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getSemVersionsWithPreRelease + */ + public function testDisallowPreReleaseRejectsPreReleaseVersions(string $version) + { + $constraint = new SemVer(allowPreRelease: false, message: 'myMessage'); + + $this->validator->validate($version, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$version.'"') + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getSemVersionsWithBuildMetadata + */ + public function testDisallowBuildMetadataRejectsBuildMetadataVersions(string $version) + { + $constraint = new SemVer(allowBuildMetadata: false, message: 'myMessage'); + + $this->validator->validate($version, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$version.'"') + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->assertRaised(); + } + + public static function getValidSemVersions(): iterable + { + // Full versions + yield ['0.0.0']; + yield ['1.0.0']; + yield ['1.2.3']; + yield ['10.20.30']; + + // Partial versions + yield ['1']; + yield ['1.2']; + yield ['10.20']; + + // With prefix + yield ['v1.0.0']; + yield ['v1.2.3']; + yield ['v1']; + yield ['v1.2']; + + // With pre-release + yield ['1.0.0-alpha']; + yield ['1.0.0-alpha.1']; + yield ['1.0.0-0.3.7']; + yield ['1.0.0-x.7.z.92']; + yield ['1.0.0-alpha+001']; + yield ['1.0.0+20130313144700']; + yield ['1.0.0-beta+exp.sha.5114f85']; + yield ['1.0.0+21AF26D3----117B344092BD']; + + // Complex examples + yield ['1.2.3-alpha.1.2+build.123']; + yield ['v1.2.3-rc.1+build.123']; + } + + public static function getValidSemVersionsWithPrefix(): iterable + { + yield ['v1.0.0']; + yield ['v1.2.3']; + yield ['v1']; + yield ['v1.2']; + yield ['v1.0.0-alpha']; + yield ['v1.0.0-alpha.1']; + yield ['v1.0.0+20130313144700']; + yield ['v1.0.0-beta+exp.sha.5114f85']; + } + + public static function getInvalidSemVersions(): iterable + { + yield ['']; + yield ['v']; + yield ['1.2.3.4']; + yield ['01.2.3']; + yield ['1.02.3']; + yield ['1.2.03']; + yield ['1.2-alpha']; + yield ['1.2.3-']; + yield ['1.2.3-+']; + yield ['1.2.3-+123']; + yield ['1.2.3-']; + yield ['+invalid']; + yield ['-invalid']; + yield ['-invalid+invalid']; + yield ['-invalid.01']; + yield ['alpha']; + yield ['1.2.3.DEV']; + yield ['1.2-SNAPSHOT']; + yield ['1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788']; + yield ['1.2-RC-SNAPSHOT']; + yield ['1.0.0+']; + yield ['1.0.0-']; + } + + public static function getInvalidSemVersionsWithoutPrefix(): iterable + { + yield ['1.0.0']; + yield ['1.2.3']; + yield ['1']; + yield ['1.2']; + yield ['1.0.0-alpha']; + yield ['1.0.0+20130313144700']; + } + + public static function getSemVersionsWithPreRelease(): iterable + { + yield ['1.0.0-alpha']; + yield ['1.0.0-alpha.1']; + yield ['1.0.0-0.3.7']; + yield ['1.0.0-x.7.z.92']; + yield ['1.0.0-alpha+001']; + yield ['1.0.0-beta+exp.sha.5114f85']; + yield ['v1.0.0-rc.1']; + yield ['v1.2.3-alpha.1.2+build.123']; + } + + public static function getSemVersionsWithBuildMetadata(): iterable + { + yield ['1.0.0+20130313144700']; + yield ['1.0.0-alpha+001']; + yield ['1.0.0-beta+exp.sha.5114f85']; + yield ['1.0.0+21AF26D3----117B344092BD']; + yield ['v1.2.3-alpha.1.2+build.123']; + yield ['v1.2.3+build.123']; + } +} \ No newline at end of file From 5a1aa4a04822b598d27f8570eb5a3004b09566c9 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 30 Jun 2025 22:46:24 +0200 Subject: [PATCH 02/28] Add newlines at EOF and improve code formatting - Add missing newlines at end of files - Break up long regex patterns with inline comments for better readability - Convert PHPDoc @dataProvider annotations to PHP 8 attributes --- .../Validator/Constraints/SemVer.php | 2 +- .../Validator/Constraints/SemVerValidator.php | 22 +++++++++++++--- .../Tests/Constraints/SemVerValidatorTest.php | 26 +++++-------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index 55ecb8b2a3550..b7b9aa432d041 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -52,4 +52,4 @@ public function __construct( $this->allowPreRelease = $allowPreRelease ?? $this->allowPreRelease; $this->allowBuildMetadata = $allowBuildMetadata ?? $this->allowBuildMetadata; } -} \ No newline at end of file +} diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 4d857eaea3791..70fb13aa1bbd3 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -26,13 +26,29 @@ class SemVerValidator extends ConstraintValidator * Supports: 1.0.0, 1.2.3, 1.2.3-alpha, 1.2.3-alpha.1, 1.2.3+20130313144700, 1.2.3-beta+exp.sha.5114f85 * With optional "v" prefix: v1.0.0, v1.2.3-alpha */ - private const SEMVER_PATTERN = '/^(?Pv)?(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/'; + private const SEMVER_PATTERN = '/^' + .'(?Pv)?' // Optional "v" prefix + .'(?P0|[1-9]\d*)' // Major version + .'\.(?P0|[1-9]\d*)' // Minor version + .'\.(?P0|[1-9]\d*)' // Patch version + .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release version + .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments + .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata + .'$/'; /** * Loose semantic versioning pattern that allows partial versions. * Supports: 1, 1.2, 1.2.3, v1, v1.2, v1.2.3, plus all the variations above */ - private const LOOSE_SEMVER_PATTERN = '/^(?Pv)?(?P0|[1-9]\d*)(?:\.(?P0|[1-9]\d*)(?:\.(?P0|[1-9]\d*))?)?(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/'; + private const LOOSE_SEMVER_PATTERN = '/^' + .'(?Pv)?' // Optional "v" prefix + .'(?P0|[1-9]\d*)' // Major version (required) + .'(?:\.(?P0|[1-9]\d*)' // Minor version (optional) + .'(?:\.(?P0|[1-9]\d*))?)?' // Patch version (optional) + .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release version + .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments + .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata + .'$/'; public function validate(mixed $value, Constraint $constraint): void { @@ -88,4 +104,4 @@ public function validate(mixed $value, Constraint $constraint): void ->addViolation(); } } -} \ No newline at end of file +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 254315b23b541..04a1beb4e621b 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -36,9 +36,7 @@ public function testEmptyStringIsValid() $this->assertNoViolation(); } - /** - * @dataProvider getValidSemVersions - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getValidSemVersions')] public function testValidSemVersions(string $version) { $this->validator->validate($version, new SemVer()); @@ -46,9 +44,7 @@ public function testValidSemVersions(string $version) $this->assertNoViolation(); } - /** - * @dataProvider getValidSemVersionsWithPrefix - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getValidSemVersionsWithPrefix')] public function testValidSemVersionsWithPrefix(string $version) { $this->validator->validate($version, new SemVer(requirePrefix: true)); @@ -56,9 +52,7 @@ public function testValidSemVersionsWithPrefix(string $version) $this->assertNoViolation(); } - /** - * @dataProvider getInvalidSemVersions - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getInvalidSemVersions')] public function testInvalidSemVersions(string $version) { $constraint = new SemVer(message: 'myMessage'); @@ -71,9 +65,7 @@ public function testInvalidSemVersions(string $version) ->assertRaised(); } - /** - * @dataProvider getInvalidSemVersionsWithoutPrefix - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getInvalidSemVersionsWithoutPrefix')] public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) { $constraint = new SemVer(requirePrefix: true, message: 'myMessage'); @@ -86,9 +78,7 @@ public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) ->assertRaised(); } - /** - * @dataProvider getSemVersionsWithPreRelease - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getSemVersionsWithPreRelease')] public function testDisallowPreReleaseRejectsPreReleaseVersions(string $version) { $constraint = new SemVer(allowPreRelease: false, message: 'myMessage'); @@ -101,9 +91,7 @@ public function testDisallowPreReleaseRejectsPreReleaseVersions(string $version) ->assertRaised(); } - /** - * @dataProvider getSemVersionsWithBuildMetadata - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getSemVersionsWithBuildMetadata')] public function testDisallowBuildMetadataRejectsBuildMetadataVersions(string $version) { $constraint = new SemVer(allowBuildMetadata: false, message: 'myMessage'); @@ -219,4 +207,4 @@ public static function getSemVersionsWithBuildMetadata(): iterable yield ['v1.2.3-alpha.1.2+build.123']; yield ['v1.2.3+build.123']; } -} \ No newline at end of file +} From 2ac6a742086b874db0a5537573510a5b338061d6 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 30 Jun 2025 22:49:17 +0200 Subject: [PATCH 03/28] Import DataProvider attribute and use short syntax --- .../Tests/Constraints/SemVerValidatorTest.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 04a1beb4e621b..a122890c9ca0e 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Tests\Constraints; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Validator\Constraints\SemVer; use Symfony\Component\Validator\Constraints\SemVerValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; @@ -36,7 +37,7 @@ public function testEmptyStringIsValid() $this->assertNoViolation(); } - #[\PHPUnit\Framework\Attributes\DataProvider('getValidSemVersions')] + #[DataProvider('getValidSemVersions')] public function testValidSemVersions(string $version) { $this->validator->validate($version, new SemVer()); @@ -44,7 +45,7 @@ public function testValidSemVersions(string $version) $this->assertNoViolation(); } - #[\PHPUnit\Framework\Attributes\DataProvider('getValidSemVersionsWithPrefix')] + #[DataProvider('getValidSemVersionsWithPrefix')] public function testValidSemVersionsWithPrefix(string $version) { $this->validator->validate($version, new SemVer(requirePrefix: true)); @@ -52,7 +53,7 @@ public function testValidSemVersionsWithPrefix(string $version) $this->assertNoViolation(); } - #[\PHPUnit\Framework\Attributes\DataProvider('getInvalidSemVersions')] + #[DataProvider('getInvalidSemVersions')] public function testInvalidSemVersions(string $version) { $constraint = new SemVer(message: 'myMessage'); @@ -65,7 +66,7 @@ public function testInvalidSemVersions(string $version) ->assertRaised(); } - #[\PHPUnit\Framework\Attributes\DataProvider('getInvalidSemVersionsWithoutPrefix')] + #[DataProvider('getInvalidSemVersionsWithoutPrefix')] public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) { $constraint = new SemVer(requirePrefix: true, message: 'myMessage'); @@ -78,7 +79,7 @@ public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) ->assertRaised(); } - #[\PHPUnit\Framework\Attributes\DataProvider('getSemVersionsWithPreRelease')] + #[DataProvider('getSemVersionsWithPreRelease')] public function testDisallowPreReleaseRejectsPreReleaseVersions(string $version) { $constraint = new SemVer(allowPreRelease: false, message: 'myMessage'); @@ -91,7 +92,7 @@ public function testDisallowPreReleaseRejectsPreReleaseVersions(string $version) ->assertRaised(); } - #[\PHPUnit\Framework\Attributes\DataProvider('getSemVersionsWithBuildMetadata')] + #[DataProvider('getSemVersionsWithBuildMetadata')] public function testDisallowBuildMetadataRejectsBuildMetadataVersions(string $version) { $constraint = new SemVer(allowBuildMetadata: false, message: 'myMessage'); From 1e3e416a4e145f145a2aba0826fb84ba42c555ed Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 30 Jun 2025 22:49:59 +0200 Subject: [PATCH 04/28] fix --- .../Validator/Constraints/SemVerValidator.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 70fb13aa1bbd3..7dfd003106deb 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -27,13 +27,13 @@ class SemVerValidator extends ConstraintValidator * With optional "v" prefix: v1.0.0, v1.2.3-alpha */ private const SEMVER_PATTERN = '/^' - .'(?Pv)?' // Optional "v" prefix - .'(?P0|[1-9]\d*)' // Major version - .'\.(?P0|[1-9]\d*)' // Minor version - .'\.(?P0|[1-9]\d*)' // Patch version - .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release version - .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments - .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata + .'(?Pv)?' // Optional "v" prefix + .'(?P0|[1-9]\d*)' // Major version + .'\.(?P0|[1-9]\d*)' // Minor version + .'\.(?P0|[1-9]\d*)' // Patch version + .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release version + .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments + .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata .'$/'; /** @@ -41,13 +41,13 @@ class SemVerValidator extends ConstraintValidator * Supports: 1, 1.2, 1.2.3, v1, v1.2, v1.2.3, plus all the variations above */ private const LOOSE_SEMVER_PATTERN = '/^' - .'(?Pv)?' // Optional "v" prefix - .'(?P0|[1-9]\d*)' // Major version (required) - .'(?:\.(?P0|[1-9]\d*)' // Minor version (optional) - .'(?:\.(?P0|[1-9]\d*))?)?' // Patch version (optional) - .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release version - .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments - .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata + .'(?Pv)?' // Optional "v" prefix + .'(?P0|[1-9]\d*)' // Major version (required) + .'(?:\.(?P0|[1-9]\d*)' // Minor version (optional) + .'(?:\.(?P0|[1-9]\d*))?)?' // Patch version (optional) + .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release version + .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments + .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata .'$/'; public function validate(mixed $value, Constraint $constraint): void From bf610d8a2b37470be79595d391037bb11cf060fb Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 30 Jun 2025 22:50:50 +0200 Subject: [PATCH 05/28] - --- .../Tests/Constraints/SemVerValidatorTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index a122890c9ca0e..9e89c09d6e5e8 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Constraints\SemVerValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -class SemVerValidatorTest extends ConstraintValidatorTestCase +final class SemVerValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): SemVerValidator { @@ -112,18 +112,18 @@ public static function getValidSemVersions(): iterable yield ['1.0.0']; yield ['1.2.3']; yield ['10.20.30']; - + // Partial versions yield ['1']; yield ['1.2']; yield ['10.20']; - + // With prefix yield ['v1.0.0']; yield ['v1.2.3']; yield ['v1']; yield ['v1.2']; - + // With pre-release yield ['1.0.0-alpha']; yield ['1.0.0-alpha.1']; @@ -133,7 +133,7 @@ public static function getValidSemVersions(): iterable yield ['1.0.0+20130313144700']; yield ['1.0.0-beta+exp.sha.5114f85']; yield ['1.0.0+21AF26D3----117B344092BD']; - + // Complex examples yield ['1.2.3-alpha.1.2+build.123']; yield ['v1.2.3-rc.1+build.123']; From 94fd2ac65f8608e7f4ae6cb241d1c1239fd8a289 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 30 Jun 2025 22:56:53 +0200 Subject: [PATCH 06/28] Fix SemVer validation pattern and tests - Fix LOOSE_SEMVER_PATTERN to only allow pre-release and build metadata with full version (major.minor.patch) - Remove empty string from invalid test cases as it's handled separately - Revert to PHPDoc @dataProvider annotations for PHPUnit 9.6 compatibility --- .../Validator/Constraints/SemVerValidator.php | 14 ++++++---- .../Tests/Constraints/SemVerValidatorTest.php | 26 +++++++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 7dfd003106deb..2d9f83128189b 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -43,11 +43,15 @@ class SemVerValidator extends ConstraintValidator private const LOOSE_SEMVER_PATTERN = '/^' .'(?Pv)?' // Optional "v" prefix .'(?P0|[1-9]\d*)' // Major version (required) - .'(?:\.(?P0|[1-9]\d*)' // Minor version (optional) - .'(?:\.(?P0|[1-9]\d*))?)?' // Patch version (optional) - .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release version - .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments - .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata + .'(?:' + .'\.(?P0|[1-9]\d*)' // Minor version + .'(?:' + .'\.(?P0|[1-9]\d*)' // Patch version + .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release (only with full version) + .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments + .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata (only with full version) + .')?' + .')?' .'$/'; public function validate(mixed $value, Constraint $constraint): void diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 9e89c09d6e5e8..5fef44f03580b 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Validator\Constraints\SemVer; use Symfony\Component\Validator\Constraints\SemVerValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; @@ -37,7 +36,9 @@ public function testEmptyStringIsValid() $this->assertNoViolation(); } - #[DataProvider('getValidSemVersions')] + /** + * @dataProvider getValidSemVersions + */ public function testValidSemVersions(string $version) { $this->validator->validate($version, new SemVer()); @@ -45,7 +46,9 @@ public function testValidSemVersions(string $version) $this->assertNoViolation(); } - #[DataProvider('getValidSemVersionsWithPrefix')] + /** + * @dataProvider getValidSemVersionsWithPrefix + */ public function testValidSemVersionsWithPrefix(string $version) { $this->validator->validate($version, new SemVer(requirePrefix: true)); @@ -53,7 +56,9 @@ public function testValidSemVersionsWithPrefix(string $version) $this->assertNoViolation(); } - #[DataProvider('getInvalidSemVersions')] + /** + * @dataProvider getInvalidSemVersions + */ public function testInvalidSemVersions(string $version) { $constraint = new SemVer(message: 'myMessage'); @@ -66,7 +71,9 @@ public function testInvalidSemVersions(string $version) ->assertRaised(); } - #[DataProvider('getInvalidSemVersionsWithoutPrefix')] + /** + * @dataProvider getInvalidSemVersionsWithoutPrefix + */ public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) { $constraint = new SemVer(requirePrefix: true, message: 'myMessage'); @@ -79,7 +86,9 @@ public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) ->assertRaised(); } - #[DataProvider('getSemVersionsWithPreRelease')] + /** + * @dataProvider getSemVersionsWithPreRelease + */ public function testDisallowPreReleaseRejectsPreReleaseVersions(string $version) { $constraint = new SemVer(allowPreRelease: false, message: 'myMessage'); @@ -92,7 +101,9 @@ public function testDisallowPreReleaseRejectsPreReleaseVersions(string $version) ->assertRaised(); } - #[DataProvider('getSemVersionsWithBuildMetadata')] + /** + * @dataProvider getSemVersionsWithBuildMetadata + */ public function testDisallowBuildMetadataRejectsBuildMetadataVersions(string $version) { $constraint = new SemVer(allowBuildMetadata: false, message: 'myMessage'); @@ -153,7 +164,6 @@ public static function getValidSemVersionsWithPrefix(): iterable public static function getInvalidSemVersions(): iterable { - yield ['']; yield ['v']; yield ['1.2.3.4']; yield ['01.2.3']; From 949d5e9688a9b0fd5dd4925ed3a564c70b17d72a Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 1 Jul 2025 09:45:50 +0200 Subject: [PATCH 07/28] Simplify SemVer constraint to use single 'strict' option - Replace requirePrefix, allowPreRelease, and allowBuildMetadata with a single 'strict' boolean option - When strict=true: follows official SemVer spec (no 'v' prefix, requires full version) - When strict=false: allows common variations (partial versions, 'v' prefix) - Update tests to reflect the new behavior --- .../Validator/Constraints/SemVer.php | 12 +- .../Validator/Constraints/SemVerValidator.php | 43 ++----- .../Tests/Constraints/SemVerValidatorTest.php | 114 ++++++------------ 3 files changed, 50 insertions(+), 119 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index b7b9aa432d041..dfca6077815ef 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -28,9 +28,7 @@ class SemVer extends Constraint ]; public string $message = 'This value is not a valid semantic version.'; - public bool $requirePrefix = false; - public bool $allowPreRelease = true; - public bool $allowBuildMetadata = true; + public bool $strict = false; /** * @param array|null $options @@ -39,17 +37,13 @@ class SemVer extends Constraint public function __construct( ?array $options = null, ?string $message = null, - ?bool $requirePrefix = null, - ?bool $allowPreRelease = null, - ?bool $allowBuildMetadata = null, + ?bool $strict = null, ?array $groups = null, mixed $payload = null, ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; - $this->requirePrefix = $requirePrefix ?? $this->requirePrefix; - $this->allowPreRelease = $allowPreRelease ?? $this->allowPreRelease; - $this->allowBuildMetadata = $allowBuildMetadata ?? $this->allowBuildMetadata; + $this->strict = $strict ?? $this->strict; } } diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 2d9f83128189b..aadad2fb66536 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -22,12 +22,11 @@ class SemVerValidator extends ConstraintValidator { /** - * Semantic Versioning 2.0.0 regex pattern. + * Strict Semantic Versioning 2.0.0 regex pattern. + * According to https://semver.org, no "v" prefix allowed * Supports: 1.0.0, 1.2.3, 1.2.3-alpha, 1.2.3-alpha.1, 1.2.3+20130313144700, 1.2.3-beta+exp.sha.5114f85 - * With optional "v" prefix: v1.0.0, v1.2.3-alpha */ - private const SEMVER_PATTERN = '/^' - .'(?Pv)?' // Optional "v" prefix + private const STRICT_SEMVER_PATTERN = '/^' .'(?P0|[1-9]\d*)' // Major version .'\.(?P0|[1-9]\d*)' // Minor version .'\.(?P0|[1-9]\d*)' // Patch version @@ -70,38 +69,10 @@ public function validate(mixed $value, Constraint $constraint): void $value = (string) $value; - // Use loose pattern by default to allow partial versions - if (!preg_match(self::LOOSE_SEMVER_PATTERN, $value, $matches)) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(SemVer::INVALID_SEMVER_ERROR) - ->addViolation(); - - return; - } - - // Check prefix requirement - if ($constraint->requirePrefix && empty($matches['prefix'])) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(SemVer::INVALID_SEMVER_ERROR) - ->addViolation(); - - return; - } - - // Check pre-release - if (!$constraint->allowPreRelease && !empty($matches['prerelease'])) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(SemVer::INVALID_SEMVER_ERROR) - ->addViolation(); - - return; - } - - // Check build metadata - if (!$constraint->allowBuildMetadata && !empty($matches['buildmetadata'])) { + // Use strict pattern (official SemVer spec) or loose pattern (common variations) + $pattern = $constraint->strict ? self::STRICT_SEMVER_PATTERN : self::LOOSE_SEMVER_PATTERN; + + if (!preg_match($pattern, $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(SemVer::INVALID_SEMVER_ERROR) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 5fef44f03580b..66d94b7085ce9 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -37,9 +37,9 @@ public function testEmptyStringIsValid() } /** - * @dataProvider getValidSemVersions + * @dataProvider getValidLooseSemVersions */ - public function testValidSemVersions(string $version) + public function testValidLooseSemVersions(string $version) { $this->validator->validate($version, new SemVer()); @@ -47,11 +47,11 @@ public function testValidSemVersions(string $version) } /** - * @dataProvider getValidSemVersionsWithPrefix + * @dataProvider getValidStrictSemVersions */ - public function testValidSemVersionsWithPrefix(string $version) + public function testValidStrictSemVersions(string $version) { - $this->validator->validate($version, new SemVer(requirePrefix: true)); + $this->validator->validate($version, new SemVer(strict: true)); $this->assertNoViolation(); } @@ -72,11 +72,11 @@ public function testInvalidSemVersions(string $version) } /** - * @dataProvider getInvalidSemVersionsWithoutPrefix + * @dataProvider getInvalidStrictSemVersions */ - public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) + public function testInvalidStrictSemVersions(string $version) { - $constraint = new SemVer(requirePrefix: true, message: 'myMessage'); + $constraint = new SemVer(strict: true, message: 'myMessage'); $this->validator->validate($version, $constraint); @@ -86,37 +86,7 @@ public function testRequirePrefixRejectsVersionsWithoutPrefix(string $version) ->assertRaised(); } - /** - * @dataProvider getSemVersionsWithPreRelease - */ - public function testDisallowPreReleaseRejectsPreReleaseVersions(string $version) - { - $constraint = new SemVer(allowPreRelease: false, message: 'myMessage'); - - $this->validator->validate($version, $constraint); - - $this->buildViolation('myMessage') - ->setParameter('{{ value }}', '"'.$version.'"') - ->setCode(SemVer::INVALID_SEMVER_ERROR) - ->assertRaised(); - } - - /** - * @dataProvider getSemVersionsWithBuildMetadata - */ - public function testDisallowBuildMetadataRejectsBuildMetadataVersions(string $version) - { - $constraint = new SemVer(allowBuildMetadata: false, message: 'myMessage'); - - $this->validator->validate($version, $constraint); - - $this->buildViolation('myMessage') - ->setParameter('{{ value }}', '"'.$version.'"') - ->setCode(SemVer::INVALID_SEMVER_ERROR) - ->assertRaised(); - } - - public static function getValidSemVersions(): iterable + public static function getValidLooseSemVersions(): iterable { // Full versions yield ['0.0.0']; @@ -150,16 +120,28 @@ public static function getValidSemVersions(): iterable yield ['v1.2.3-rc.1+build.123']; } - public static function getValidSemVersionsWithPrefix(): iterable + public static function getValidStrictSemVersions(): iterable { - yield ['v1.0.0']; - yield ['v1.2.3']; - yield ['v1']; - yield ['v1.2']; - yield ['v1.0.0-alpha']; - yield ['v1.0.0-alpha.1']; - yield ['v1.0.0+20130313144700']; - yield ['v1.0.0-beta+exp.sha.5114f85']; + // Only valid according to official SemVer spec (no v prefix) + yield ['0.0.0']; + yield ['1.0.0']; + yield ['1.2.3']; + yield ['10.20.30']; + + // With pre-release + yield ['1.0.0-alpha']; + yield ['1.0.0-alpha.1']; + yield ['1.0.0-0.3.7']; + yield ['1.0.0-x.7.z.92']; + + // With build metadata + yield ['1.0.0+20130313144700']; + yield ['1.0.0+21AF26D3----117B344092BD']; + + // With both + yield ['1.0.0-alpha+001']; + yield ['1.0.0-beta+exp.sha.5114f85']; + yield ['1.2.3-alpha.1.2+build.123']; } public static function getInvalidSemVersions(): iterable @@ -187,35 +169,19 @@ public static function getInvalidSemVersions(): iterable yield ['1.0.0-']; } - public static function getInvalidSemVersionsWithoutPrefix(): iterable + public static function getInvalidStrictSemVersions(): iterable { - yield ['1.0.0']; - yield ['1.2.3']; + // Versions with v prefix (not allowed in strict mode) + yield ['v1.0.0']; + yield ['v1.2.3']; + yield ['v1.0.0-alpha']; + yield ['v1.0.0+20130313144700']; + + // Partial versions (not allowed in strict mode) yield ['1']; yield ['1.2']; - yield ['1.0.0-alpha']; - yield ['1.0.0+20130313144700']; - } - - public static function getSemVersionsWithPreRelease(): iterable - { - yield ['1.0.0-alpha']; - yield ['1.0.0-alpha.1']; - yield ['1.0.0-0.3.7']; - yield ['1.0.0-x.7.z.92']; - yield ['1.0.0-alpha+001']; - yield ['1.0.0-beta+exp.sha.5114f85']; - yield ['v1.0.0-rc.1']; - yield ['v1.2.3-alpha.1.2+build.123']; + yield ['v1']; + yield ['v1.2']; } - public static function getSemVersionsWithBuildMetadata(): iterable - { - yield ['1.0.0+20130313144700']; - yield ['1.0.0-alpha+001']; - yield ['1.0.0-beta+exp.sha.5114f85']; - yield ['1.0.0+21AF26D3----117B344092BD']; - yield ['v1.2.3-alpha.1.2+build.123']; - yield ['v1.2.3+build.123']; - } } From c287e8d60751a21338f32336044db96dd305ad6c Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 1 Jul 2025 09:49:01 +0200 Subject: [PATCH 08/28] Update src/Symfony/Component/Validator/Constraints/SemVerValidator.php --- src/Symfony/Component/Validator/Constraints/SemVerValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index aadad2fb66536..9212c81949ebf 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -46,7 +46,7 @@ class SemVerValidator extends ConstraintValidator .'\.(?P0|[1-9]\d*)' // Minor version .'(?:' .'\.(?P0|[1-9]\d*)' // Patch version - .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release (only with full version) + .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release (only with full version) .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata (only with full version) .')?' From 4e5ec8be76736be036ba66363ce2676dbbc6d92b Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 1 Jul 2025 09:50:03 +0200 Subject: [PATCH 09/28] Change default value of strict option to true - Default behavior now follows the official SemVer specification - Users must explicitly set strict=false to allow loose validation --- src/Symfony/Component/Validator/Constraints/SemVer.php | 2 +- .../Validator/Tests/Constraints/SemVerValidatorTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index dfca6077815ef..c52fe3cb55a54 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -28,7 +28,7 @@ class SemVer extends Constraint ]; public string $message = 'This value is not a valid semantic version.'; - public bool $strict = false; + public bool $strict = true; /** * @param array|null $options diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 66d94b7085ce9..46c01edb5d9af 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -41,7 +41,7 @@ public function testEmptyStringIsValid() */ public function testValidLooseSemVersions(string $version) { - $this->validator->validate($version, new SemVer()); + $this->validator->validate($version, new SemVer(strict: false)); $this->assertNoViolation(); } @@ -51,7 +51,7 @@ public function testValidLooseSemVersions(string $version) */ public function testValidStrictSemVersions(string $version) { - $this->validator->validate($version, new SemVer(strict: true)); + $this->validator->validate($version, new SemVer()); $this->assertNoViolation(); } @@ -76,7 +76,7 @@ public function testInvalidSemVersions(string $version) */ public function testInvalidStrictSemVersions(string $version) { - $constraint = new SemVer(strict: true, message: 'myMessage'); + $constraint = new SemVer(message: 'myMessage'); $this->validator->validate($version, $constraint); From a1e9e3a52c503d5b561a04289564fb560cf80176 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 1 Jul 2025 09:53:58 +0200 Subject: [PATCH 10/28] Remove array-based configuration support from SemVer constraint - Remove off on off off off off off off off off on off on off on off off off on off off off on on off off off on on off off off off off off off on off off off off on off on off off off off off on off on on off off off on off on on off on on off off on on on off on on off on off off off off on off off off on off off on off on off off off on on off on off on off off on off off off on off off off off off off off off on off on off on on on off on on off off on off on on on off on on off on off on off off off off off on on off off on off off off off off on off off on on off on off off on off off off on off off off off on on off on off off off off off on off on off off off off off off off off off off on on off on off off off parameter from constructor as array-based configuration is deprecated in Symfony 8.0 - Only support named arguments for configuration - This follows the modern Symfony constraint pattern --- src/Symfony/Component/Validator/Constraints/SemVer.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index c52fe3cb55a54..d25e629da2cf3 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -31,17 +31,15 @@ class SemVer extends Constraint public bool $strict = true; /** - * @param array|null $options * @param string[]|null $groups */ public function __construct( - ?array $options = null, ?string $message = null, ?bool $strict = null, ?array $groups = null, mixed $payload = null, ) { - parent::__construct($options, $groups, $payload); + parent::__construct(null, $groups, $payload); $this->message = $message ?? $this->message; $this->strict = $strict ?? $this->strict; From 980da3bd2bbd33defac7d17743d1d7fd541a89ac Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 1 Jul 2025 09:54:32 +0200 Subject: [PATCH 11/28] Remove unnecessary comment --- src/Symfony/Component/Validator/Constraints/SemVerValidator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 9212c81949ebf..1b68c8e767b99 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -69,7 +69,6 @@ public function validate(mixed $value, Constraint $constraint): void $value = (string) $value; - // Use strict pattern (official SemVer spec) or loose pattern (common variations) $pattern = $constraint->strict ? self::STRICT_SEMVER_PATTERN : self::LOOSE_SEMVER_PATTERN; if (!preg_match($pattern, $value)) { From 24ae6af3a1a6392c70ffa675519305f192d2f630 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 1 Jul 2025 10:11:14 +0200 Subject: [PATCH 12/28] Use x modifier for regex patterns to improve readability - Convert regex patterns to multi-line format with x modifier - Add inline comments to explain each part of the pattern - Makes complex regex patterns more maintainable --- .../Validator/Constraints/SemVerValidator.php | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 1b68c8e767b99..7ab9c3ef89fcb 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -26,32 +26,52 @@ class SemVerValidator extends ConstraintValidator * According to https://semver.org, no "v" prefix allowed * Supports: 1.0.0, 1.2.3, 1.2.3-alpha, 1.2.3-alpha.1, 1.2.3+20130313144700, 1.2.3-beta+exp.sha.5114f85 */ - private const STRICT_SEMVER_PATTERN = '/^' - .'(?P0|[1-9]\d*)' // Major version - .'\.(?P0|[1-9]\d*)' // Minor version - .'\.(?P0|[1-9]\d*)' // Patch version - .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release version - .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments - .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata - .'$/'; + private const STRICT_SEMVER_PATTERN = '/^ + (?P0|[1-9]\d*) # Major version + \. + (?P0|[1-9]\d*) # Minor version + \. + (?P0|[1-9]\d*) # Patch version + (?: + - + (?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # Pre-release identifier + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* # Additional dot-separated identifiers + ) + )? + (?: + \+ + (?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*) # Build metadata + )? + $/x'; /** * Loose semantic versioning pattern that allows partial versions. * Supports: 1, 1.2, 1.2.3, v1, v1.2, v1.2.3, plus all the variations above */ - private const LOOSE_SEMVER_PATTERN = '/^' - .'(?Pv)?' // Optional "v" prefix - .'(?P0|[1-9]\d*)' // Major version (required) - .'(?:' - .'\.(?P0|[1-9]\d*)' // Minor version - .'(?:' - .'\.(?P0|[1-9]\d*)' // Patch version - .'(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' // Pre-release (only with full version) - .'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' // Pre-release segments - .'(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' // Build metadata (only with full version) - .')?' - .')?' - .'$/'; + private const LOOSE_SEMVER_PATTERN = '/^ + (?Pv)? # Optional "v" prefix + (?P0|[1-9]\d*) # Major version (required) + (?: + \. + (?P0|[1-9]\d*) # Minor version (optional) + (?: + \. + (?P0|[1-9]\d*) # Patch version (optional) + (?: + - + (?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # Pre-release identifier + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* # Additional identifiers + ) + )? + (?: + \+ + (?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*) # Build metadata + )? + )? + )? + $/x'; public function validate(mixed $value, Constraint $constraint): void { From 29fa35a382244a32623449f06a86d078e38e72a6 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 1 Jul 2025 10:12:09 +0200 Subject: [PATCH 13/28] Apply stof's suggestions for better compatibility - Add HasNamedArguments attribute for XML/YAML mapping compatibility - Move property defaults to constructor arguments as non-nullable - Follow modern Symfony constraint pattern --- .../Component/Validator/Constraints/SemVer.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index d25e629da2cf3..c628dd1a5c1f8 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** @@ -27,21 +28,22 @@ class SemVer extends Constraint self::INVALID_SEMVER_ERROR => 'INVALID_SEMVER_ERROR', ]; - public string $message = 'This value is not a valid semantic version.'; - public bool $strict = true; + public string $message; + public bool $strict; /** * @param string[]|null $groups */ + #[HasNamedArguments] public function __construct( - ?string $message = null, - ?bool $strict = null, + string $message = 'This value is not a valid semantic version.', + bool $strict = true, ?array $groups = null, mixed $payload = null, ) { parent::__construct(null, $groups, $payload); - $this->message = $message ?? $this->message; - $this->strict = $strict ?? $this->strict; + $this->message = $message; + $this->strict = $strict; } } From a1150a8a614dd6320719439d1d5e709dd0340a70 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 1 Jul 2025 10:28:54 +0200 Subject: [PATCH 14/28] Apply suggestions from code review --- .../Component/Validator/Constraints/SemVerValidator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 7ab9c3ef89fcb..a9c3157481b3e 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -36,7 +36,7 @@ class SemVerValidator extends ConstraintValidator - (?P (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # Pre-release identifier - (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* # Additional dot-separated identifiers + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* # Additional dot-separated identifiers ) )? (?: @@ -62,7 +62,7 @@ class SemVerValidator extends ConstraintValidator - (?P (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # Pre-release identifier - (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* # Additional identifiers + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* # Additional identifiers ) )? (?: From 16a50f5c6378e8f39841260bd7c89feac53fa15a Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 2 Jul 2025 22:27:59 +0200 Subject: [PATCH 15/28] Update src/Symfony/Component/Validator/Constraints/SemVer.php Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> --- src/Symfony/Component/Validator/Constraints/SemVer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index c628dd1a5c1f8..a70573ac3f54d 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -17,6 +17,8 @@ /** * Validates that a value is a valid semantic version. * + * @see https://semver.org + * * @author Oskar Stark */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] From eb7cb3a2c9ec140d3e8b0f598791eb615ce29170 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 2 Jul 2025 22:33:57 +0200 Subject: [PATCH 16/28] Add min/max version constraints to SemVer validator - Add min and max parameters to SemVer constraint - Implement version comparison logic using version_compare() - Add normalizeVersion() method to handle version normalization - Add comprehensive test coverage for min/max functionality - Validate min/max parameters follow strict mode rules --- .../Validator/Constraints/SemVer.php | 16 +++ .../Validator/Constraints/SemVerValidator.php | 65 +++++++++ .../Tests/Constraints/SemVerValidatorTest.php | 133 ++++++++++++++++++ 3 files changed, 214 insertions(+) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index a70573ac3f54d..561887fdb5e25 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -25,13 +25,21 @@ class SemVer extends Constraint { public const INVALID_SEMVER_ERROR = '3e7a8b8f-4d8f-4c7a-b5e9-1a2b3c4d5e6f'; + public const TOO_LOW_ERROR = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789'; + public const TOO_HIGH_ERROR = 'b1c2d3e4-f5a6-7890-bcde-f01234567890'; protected const ERROR_NAMES = [ self::INVALID_SEMVER_ERROR => 'INVALID_SEMVER_ERROR', + self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', + self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', ]; public string $message; + public string $tooLowMessage; + public string $tooHighMessage; public bool $strict; + public ?string $min; + public ?string $max; /** * @param string[]|null $groups @@ -39,13 +47,21 @@ class SemVer extends Constraint #[HasNamedArguments] public function __construct( string $message = 'This value is not a valid semantic version.', + string $tooLowMessage = 'This value should be {{ min }} or more.', + string $tooHighMessage = 'This value should be {{ max }} or less.', bool $strict = true, + ?string $min = null, + ?string $max = null, ?array $groups = null, mixed $payload = null, ) { parent::__construct(null, $groups, $payload); $this->message = $message; + $this->tooLowMessage = $tooLowMessage; + $this->tooHighMessage = $tooHighMessage; $this->strict = $strict; + $this->min = $min; + $this->max = $max; } } diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index a9c3157481b3e..6053c6973de24 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -96,6 +96,71 @@ public function validate(mixed $value, Constraint $constraint): void ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(SemVer::INVALID_SEMVER_ERROR) ->addViolation(); + + return; } + + // Normalize the version for comparison (remove 'v' prefix if present) + $normalizedValue = $this->normalizeVersion($value); + + // Check min constraint + if (null !== $constraint->min) { + $normalizedMin = $this->normalizeVersion($constraint->min); + + if (!preg_match($pattern, $constraint->min)) { + throw new \InvalidArgumentException(sprintf('The "min" option value "%s" is not a valid semantic version according to the "strict" option.', $constraint->min)); + } + + if (version_compare($normalizedValue, $normalizedMin, '<')) { + $this->context->buildViolation($constraint->tooLowMessage) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ min }}', $constraint->min) + ->setCode(SemVer::TOO_LOW_ERROR) + ->addViolation(); + } + } + + // Check max constraint + if (null !== $constraint->max) { + $normalizedMax = $this->normalizeVersion($constraint->max); + + if (!preg_match($pattern, $constraint->max)) { + throw new \InvalidArgumentException(sprintf('The "max" option value "%s" is not a valid semantic version according to the "strict" option.', $constraint->max)); + } + + if (version_compare($normalizedValue, $normalizedMax, '>')) { + $this->context->buildViolation($constraint->tooHighMessage) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ max }}', $constraint->max) + ->setCode(SemVer::TOO_HIGH_ERROR) + ->addViolation(); + } + } + } + + /** + * Normalizes a version string for comparison by removing the 'v' prefix and + * ensuring it has all three version components (major.minor.patch). + */ + private function normalizeVersion(string $version): string + { + // Remove 'v' prefix if present + $version = ltrim($version, 'v'); + + // Split into parts + $parts = explode('.', explode('-', explode('+', $version)[0])[0]); + + // Ensure we have at least 3 parts for version_compare + while (count($parts) < 3) { + $parts[] = '0'; + } + + // Get pre-release and build metadata if any + $suffix = ''; + if (preg_match('/^[^-+]+(.+)$/', $version, $matches)) { + $suffix = $matches[1]; + } + + return implode('.', array_slice($parts, 0, 3)) . $suffix; } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 46c01edb5d9af..15496f5baf435 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -184,4 +184,137 @@ public static function getInvalidStrictSemVersions(): iterable yield ['v1.2']; } + /** + * @dataProvider getValidVersionsWithMinMax + */ + public function testValidVersionsWithMinMax(string $version, ?string $min, ?string $max, bool $strict = true) + { + $constraint = new SemVer(strict: $strict, min: $min, max: $max); + + $this->validator->validate($version, $constraint); + + $this->assertNoViolation(); + } + + public static function getValidVersionsWithMinMax(): iterable + { + // Test min only + yield ['2.0.0', '1.0.0', null]; + yield ['2.0.0', '2.0.0', null]; + yield ['2.0.1', '2.0.0', null]; + + // Test max only + yield ['1.0.0', null, '2.0.0']; + yield ['2.0.0', null, '2.0.0']; + yield ['1.9.9', null, '2.0.0']; + + // Test both min and max + yield ['1.5.0', '1.0.0', '2.0.0']; + yield ['1.0.0', '1.0.0', '2.0.0']; + yield ['2.0.0', '1.0.0', '2.0.0']; + + // Test with pre-release versions + yield ['1.0.0-alpha', '1.0.0-alpha', null]; + yield ['1.0.0', '1.0.0-alpha', null]; + yield ['1.0.0-beta', '1.0.0-alpha', null]; + yield ['1.0.0-alpha.2', '1.0.0-alpha.1', null]; + + // Test with loose versions + yield ['v2.0', 'v1.0', null, false]; + yield ['2', '1', null, false]; + yield ['v1.5', 'v1.0', 'v2.0', false]; + } + + /** + * @dataProvider getTooLowVersions + */ + public function testTooLowVersions(string $version, string $min, bool $strict = true) + { + $constraint = new SemVer( + tooLowMessage: 'myMessage', + strict: $strict, + min: $min + ); + + $this->validator->validate($version, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$version.'"') + ->setParameter('{{ min }}', $min) + ->setCode(SemVer::TOO_LOW_ERROR) + ->assertRaised(); + } + + public static function getTooLowVersions(): iterable + { + yield ['0.9.9', '1.0.0']; + yield ['1.0.0', '1.0.1']; + yield ['1.0.0-alpha', '1.0.0']; + yield ['1.0.0-alpha.1', '1.0.0-alpha.2']; + + // Test with loose versions + yield ['v0.9', 'v1.0', false]; + yield ['1', '2', false]; + } + + /** + * @dataProvider getTooHighVersions + */ + public function testTooHighVersions(string $version, string $max, bool $strict = true) + { + $constraint = new SemVer( + tooHighMessage: 'myMessage', + strict: $strict, + max: $max + ); + + $this->validator->validate($version, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$version.'"') + ->setParameter('{{ max }}', $max) + ->setCode(SemVer::TOO_HIGH_ERROR) + ->assertRaised(); + } + + public static function getTooHighVersions(): iterable + { + yield ['2.0.1', '2.0.0']; + yield ['1.0.1', '1.0.0']; + yield ['1.0.0', '1.0.0-alpha']; + yield ['1.0.0-alpha.2', '1.0.0-alpha.1']; + + // Test with loose versions + yield ['v2.1', 'v2.0', false]; + yield ['3', '2', false]; + } + + public function testInvalidMinOption() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "min" option value "invalid" is not a valid semantic version according to the "strict" option.'); + + $constraint = new SemVer(min: 'invalid'); + $this->validator->validate('1.0.0', $constraint); + } + + public function testInvalidMaxOption() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "max" option value "invalid" is not a valid semantic version according to the "strict" option.'); + + $constraint = new SemVer(max: 'invalid'); + $this->validator->validate('1.0.0', $constraint); + } + + public function testMinMaxOptionsFollowStrictMode() + { + // In strict mode, min/max with 'v' prefix should be invalid + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "min" option value "v1.0.0" is not a valid semantic version according to the "strict" option.'); + + $constraint = new SemVer(strict: true, min: 'v1.0.0'); + $this->validator->validate('2.0.0', $constraint); + } + } From c662126ed53e2baf93238a980499ae8de2b0cb15 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 2 Jul 2025 22:39:26 +0200 Subject: [PATCH 17/28] Rename tooHighMessage/tooLowMessage to minMessage/maxMessage and make strict parameter required in data providers --- .../Validator/Constraints/SemVer.php | 12 ++--- .../Validator/Constraints/SemVerValidator.php | 4 +- .../Tests/Constraints/SemVerValidatorTest.php | 52 +++++++++---------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index 561887fdb5e25..4c9d443786cc0 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -35,8 +35,8 @@ class SemVer extends Constraint ]; public string $message; - public string $tooLowMessage; - public string $tooHighMessage; + public string $minMessage; + public string $maxMessage; public bool $strict; public ?string $min; public ?string $max; @@ -47,8 +47,8 @@ class SemVer extends Constraint #[HasNamedArguments] public function __construct( string $message = 'This value is not a valid semantic version.', - string $tooLowMessage = 'This value should be {{ min }} or more.', - string $tooHighMessage = 'This value should be {{ max }} or less.', + string $minMessage = 'This value should be {{ min }} or more.', + string $maxMessage = 'This value should be {{ max }} or less.', bool $strict = true, ?string $min = null, ?string $max = null, @@ -58,8 +58,8 @@ public function __construct( parent::__construct(null, $groups, $payload); $this->message = $message; - $this->tooLowMessage = $tooLowMessage; - $this->tooHighMessage = $tooHighMessage; + $this->minMessage = $minMessage; + $this->maxMessage = $maxMessage; $this->strict = $strict; $this->min = $min; $this->max = $max; diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 6053c6973de24..3d2b91b4edeca 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -112,7 +112,7 @@ public function validate(mixed $value, Constraint $constraint): void } if (version_compare($normalizedValue, $normalizedMin, '<')) { - $this->context->buildViolation($constraint->tooLowMessage) + $this->context->buildViolation($constraint->minMessage) ->setParameter('{{ value }}', $this->formatValue($value)) ->setParameter('{{ min }}', $constraint->min) ->setCode(SemVer::TOO_LOW_ERROR) @@ -129,7 +129,7 @@ public function validate(mixed $value, Constraint $constraint): void } if (version_compare($normalizedValue, $normalizedMax, '>')) { - $this->context->buildViolation($constraint->tooHighMessage) + $this->context->buildViolation($constraint->maxMessage) ->setParameter('{{ value }}', $this->formatValue($value)) ->setParameter('{{ max }}', $constraint->max) ->setCode(SemVer::TOO_HIGH_ERROR) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 15496f5baf435..4410f3d3217f7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -187,7 +187,7 @@ public static function getInvalidStrictSemVersions(): iterable /** * @dataProvider getValidVersionsWithMinMax */ - public function testValidVersionsWithMinMax(string $version, ?string $min, ?string $max, bool $strict = true) + public function testValidVersionsWithMinMax(string $version, ?string $min, ?string $max, bool $strict) { $constraint = new SemVer(strict: $strict, min: $min, max: $max); @@ -199,25 +199,25 @@ public function testValidVersionsWithMinMax(string $version, ?string $min, ?stri public static function getValidVersionsWithMinMax(): iterable { // Test min only - yield ['2.0.0', '1.0.0', null]; - yield ['2.0.0', '2.0.0', null]; - yield ['2.0.1', '2.0.0', null]; + yield ['2.0.0', '1.0.0', null, true]; + yield ['2.0.0', '2.0.0', null, true]; + yield ['2.0.1', '2.0.0', null, true]; // Test max only - yield ['1.0.0', null, '2.0.0']; - yield ['2.0.0', null, '2.0.0']; - yield ['1.9.9', null, '2.0.0']; + yield ['1.0.0', null, '2.0.0', true]; + yield ['2.0.0', null, '2.0.0', true]; + yield ['1.9.9', null, '2.0.0', true]; // Test both min and max - yield ['1.5.0', '1.0.0', '2.0.0']; - yield ['1.0.0', '1.0.0', '2.0.0']; - yield ['2.0.0', '1.0.0', '2.0.0']; + yield ['1.5.0', '1.0.0', '2.0.0', true]; + yield ['1.0.0', '1.0.0', '2.0.0', true]; + yield ['2.0.0', '1.0.0', '2.0.0', true]; // Test with pre-release versions - yield ['1.0.0-alpha', '1.0.0-alpha', null]; - yield ['1.0.0', '1.0.0-alpha', null]; - yield ['1.0.0-beta', '1.0.0-alpha', null]; - yield ['1.0.0-alpha.2', '1.0.0-alpha.1', null]; + yield ['1.0.0-alpha', '1.0.0-alpha', null, true]; + yield ['1.0.0', '1.0.0-alpha', null, true]; + yield ['1.0.0-beta', '1.0.0-alpha', null, true]; + yield ['1.0.0-alpha.2', '1.0.0-alpha.1', null, true]; // Test with loose versions yield ['v2.0', 'v1.0', null, false]; @@ -228,10 +228,10 @@ public static function getValidVersionsWithMinMax(): iterable /** * @dataProvider getTooLowVersions */ - public function testTooLowVersions(string $version, string $min, bool $strict = true) + public function testTooLowVersions(string $version, string $min, bool $strict) { $constraint = new SemVer( - tooLowMessage: 'myMessage', + minMessage: 'myMessage', strict: $strict, min: $min ); @@ -247,10 +247,10 @@ public function testTooLowVersions(string $version, string $min, bool $strict = public static function getTooLowVersions(): iterable { - yield ['0.9.9', '1.0.0']; - yield ['1.0.0', '1.0.1']; - yield ['1.0.0-alpha', '1.0.0']; - yield ['1.0.0-alpha.1', '1.0.0-alpha.2']; + yield ['0.9.9', '1.0.0', true]; + yield ['1.0.0', '1.0.1', true]; + yield ['1.0.0-alpha', '1.0.0', true]; + yield ['1.0.0-alpha.1', '1.0.0-alpha.2', true]; // Test with loose versions yield ['v0.9', 'v1.0', false]; @@ -260,10 +260,10 @@ public static function getTooLowVersions(): iterable /** * @dataProvider getTooHighVersions */ - public function testTooHighVersions(string $version, string $max, bool $strict = true) + public function testTooHighVersions(string $version, string $max, bool $strict) { $constraint = new SemVer( - tooHighMessage: 'myMessage', + maxMessage: 'myMessage', strict: $strict, max: $max ); @@ -279,10 +279,10 @@ public function testTooHighVersions(string $version, string $max, bool $strict = public static function getTooHighVersions(): iterable { - yield ['2.0.1', '2.0.0']; - yield ['1.0.1', '1.0.0']; - yield ['1.0.0', '1.0.0-alpha']; - yield ['1.0.0-alpha.2', '1.0.0-alpha.1']; + yield ['2.0.1', '2.0.0', true]; + yield ['1.0.1', '1.0.0', true]; + yield ['1.0.0', '1.0.0-alpha', true]; + yield ['1.0.0-alpha.2', '1.0.0-alpha.1', true]; // Test with loose versions yield ['v2.1', 'v2.0', false]; From b944f6927da0e6e2e6a91f63c2f02f3e25978d07 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 2 Jul 2025 22:40:13 +0200 Subject: [PATCH 18/28] Update src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php --- .../Validator/Tests/Constraints/SemVerValidatorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 4410f3d3217f7..1c4a5152a4f5e 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -316,5 +316,4 @@ public function testMinMaxOptionsFollowStrictMode() $constraint = new SemVer(strict: true, min: 'v1.0.0'); $this->validator->validate('2.0.0', $constraint); } - } From 2481babf83aa3fa9ff4fffe0429a21987e680342 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 2 Jul 2025 22:41:27 +0200 Subject: [PATCH 19/28] Move expectException calls directly before the code that triggers the exception --- .../Tests/Constraints/SemVerValidatorTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 1c4a5152a4f5e..30159479c6de8 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -291,29 +291,29 @@ public static function getTooHighVersions(): iterable public function testInvalidMinOption() { + $constraint = new SemVer(min: 'invalid'); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The "min" option value "invalid" is not a valid semantic version according to the "strict" option.'); - - $constraint = new SemVer(min: 'invalid'); $this->validator->validate('1.0.0', $constraint); } public function testInvalidMaxOption() { + $constraint = new SemVer(max: 'invalid'); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The "max" option value "invalid" is not a valid semantic version according to the "strict" option.'); - - $constraint = new SemVer(max: 'invalid'); $this->validator->validate('1.0.0', $constraint); } public function testMinMaxOptionsFollowStrictMode() { // In strict mode, min/max with 'v' prefix should be invalid + $constraint = new SemVer(strict: true, min: 'v1.0.0'); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The "min" option value "v1.0.0" is not a valid semantic version according to the "strict" option.'); - - $constraint = new SemVer(strict: true, min: 'v1.0.0'); $this->validator->validate('2.0.0', $constraint); } } From ccf636442f149dcf9659cb7478d03dd372614acf Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 2 Jul 2025 22:44:57 +0200 Subject: [PATCH 20/28] Add tests for custom message, minMessage and maxMessage --- .../Tests/Constraints/SemVerValidatorTest.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 30159479c6de8..6e318e982e0ef 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -86,6 +86,50 @@ public function testInvalidStrictSemVersions(string $version) ->assertRaised(); } + public function testCustomInvalidMessage() + { + $constraint = new SemVer(message: 'Custom invalid version message'); + + $this->validator->validate('invalid-version', $constraint); + + $this->buildViolation('Custom invalid version message') + ->setParameter('{{ value }}', '"invalid-version"') + ->setCode(SemVer::INVALID_SEMVER_ERROR) + ->assertRaised(); + } + + public function testCustomMinMessage() + { + $constraint = new SemVer( + minMessage: 'Custom minimum version message', + min: '2.0.0' + ); + + $this->validator->validate('1.0.0', $constraint); + + $this->buildViolation('Custom minimum version message') + ->setParameter('{{ value }}', '"1.0.0"') + ->setParameter('{{ min }}', '2.0.0') + ->setCode(SemVer::TOO_LOW_ERROR) + ->assertRaised(); + } + + public function testCustomMaxMessage() + { + $constraint = new SemVer( + maxMessage: 'Custom maximum version message', + max: '1.0.0' + ); + + $this->validator->validate('2.0.0', $constraint); + + $this->buildViolation('Custom maximum version message') + ->setParameter('{{ value }}', '"2.0.0"') + ->setParameter('{{ max }}', '1.0.0') + ->setCode(SemVer::TOO_HIGH_ERROR) + ->assertRaised(); + } + public static function getValidLooseSemVersions(): iterable { // Full versions From 5fc311999d6e70cb42b7454822ba7deac3d7f482 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 2 Jul 2025 22:48:30 +0200 Subject: [PATCH 21/28] Follow Symfony constraint pattern: initialize message properties with default values --- .../Validator/Constraints/SemVer.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index 4c9d443786cc0..5361249a88aaf 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -34,22 +34,22 @@ class SemVer extends Constraint self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', ]; - public string $message; - public string $minMessage; - public string $maxMessage; - public bool $strict; - public ?string $min; - public ?string $max; + public string $message = 'This value is not a valid semantic version.'; + public string $minMessage = 'This value should be {{ min }} or more.'; + public string $maxMessage = 'This value should be {{ max }} or less.'; + public bool $strict = true; + public ?string $min = null; + public ?string $max = null; /** * @param string[]|null $groups */ #[HasNamedArguments] public function __construct( - string $message = 'This value is not a valid semantic version.', - string $minMessage = 'This value should be {{ min }} or more.', - string $maxMessage = 'This value should be {{ max }} or less.', - bool $strict = true, + ?string $message = null, + ?string $minMessage = null, + ?string $maxMessage = null, + ?bool $strict = null, ?string $min = null, ?string $max = null, ?array $groups = null, @@ -57,10 +57,10 @@ public function __construct( ) { parent::__construct(null, $groups, $payload); - $this->message = $message; - $this->minMessage = $minMessage; - $this->maxMessage = $maxMessage; - $this->strict = $strict; + $this->message = $message ?? $this->message; + $this->minMessage = $minMessage ?? $this->minMessage; + $this->maxMessage = $maxMessage ?? $this->maxMessage; + $this->strict = $strict ?? $this->strict; $this->min = $min; $this->max = $max; } From 7b2c9d22aa76d9cf4993a44ccf884af752cf935e Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Thu, 3 Jul 2025 08:51:48 +0200 Subject: [PATCH 22/28] Update src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> --- .../Validator/Tests/Constraints/SemVerValidatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 6e318e982e0ef..26a855584cb3c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -215,7 +215,7 @@ public static function getInvalidSemVersions(): iterable public static function getInvalidStrictSemVersions(): iterable { - // Versions with v prefix (not allowed in strict mode) + // Versions with v prefix yield ['v1.0.0']; yield ['v1.2.3']; yield ['v1.0.0-alpha']; From dd3a7729dec193dd0e3bc8ab969451c13c44af83 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Thu, 3 Jul 2025 08:51:57 +0200 Subject: [PATCH 23/28] Update src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> --- .../Validator/Tests/Constraints/SemVerValidatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php index 26a855584cb3c..464220e6e2f55 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SemVerValidatorTest.php @@ -221,7 +221,7 @@ public static function getInvalidStrictSemVersions(): iterable yield ['v1.0.0-alpha']; yield ['v1.0.0+20130313144700']; - // Partial versions (not allowed in strict mode) + // Partial versions yield ['1']; yield ['1.2']; yield ['v1']; From bcd66da700d61e28d285c657b4be1a56fee332ac Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Thu, 3 Jul 2025 09:06:44 +0200 Subject: [PATCH 24/28] Remove unnecessary comments as requested in code review As per Alex Daubois's review comments, removed explanatory comments that were deemed unnecessary for clean, self-documenting code. --- .../Component/Validator/Constraints/SemVerValidator.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 3d2b91b4edeca..9f1f9c4128096 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -100,10 +100,8 @@ public function validate(mixed $value, Constraint $constraint): void return; } - // Normalize the version for comparison (remove 'v' prefix if present) $normalizedValue = $this->normalizeVersion($value); - // Check min constraint if (null !== $constraint->min) { $normalizedMin = $this->normalizeVersion($constraint->min); @@ -120,7 +118,6 @@ public function validate(mixed $value, Constraint $constraint): void } } - // Check max constraint if (null !== $constraint->max) { $normalizedMax = $this->normalizeVersion($constraint->max); @@ -144,13 +141,10 @@ public function validate(mixed $value, Constraint $constraint): void */ private function normalizeVersion(string $version): string { - // Remove 'v' prefix if present $version = ltrim($version, 'v'); - // Split into parts $parts = explode('.', explode('-', explode('+', $version)[0])[0]); - // Ensure we have at least 3 parts for version_compare while (count($parts) < 3) { $parts[] = '0'; } From f87dff0d9300871e61d57325118408ec77bafdb3 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 7 Jul 2025 12:50:06 +0200 Subject: [PATCH 25/28] Apply code review feedback from fabpot and nicolas-grekas - Add backslash to count() function for consistency - Refactor regex matching to use ternary operator for more concise code - Note: Data providers were already correctly placed after their test methods - Note: Min/max constraints implementation is complete and working as expected --- .../Component/Validator/Constraints/SemVerValidator.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php index 9f1f9c4128096..e1dfca92624d2 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVerValidator.php +++ b/src/Symfony/Component/Validator/Constraints/SemVerValidator.php @@ -145,15 +145,12 @@ private function normalizeVersion(string $version): string $parts = explode('.', explode('-', explode('+', $version)[0])[0]); - while (count($parts) < 3) { + while (\count($parts) < 3) { $parts[] = '0'; } // Get pre-release and build metadata if any - $suffix = ''; - if (preg_match('/^[^-+]+(.+)$/', $version, $matches)) { - $suffix = $matches[1]; - } + $suffix = preg_match('/^[^-+]+(.+)$/', $version, $matches) ? $matches[1] : ''; return implode('.', array_slice($parts, 0, 3)) . $suffix; } From 2af8ecea91151f24c379e18f450913e3d21ee101 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 7 Jul 2025 14:49:34 +0200 Subject: [PATCH 26/28] Add documentation for SemVer constraint options Document the available options (strict, min, max) in the class header to make it clearer what configuration options are available. --- src/Symfony/Component/Validator/Constraints/SemVer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index 5361249a88aaf..38caa6c3ca6cb 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -19,6 +19,11 @@ * * @see https://semver.org * + * Available options: + * - strict: Whether to validate strict semantic versioning (default: true) + * - min: Minimum version constraint (inclusive) + * - max: Maximum version constraint (inclusive) + * * @author Oskar Stark */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] From 8d04e1ee682f23bf3cf82f29f4ab1233382fae84 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 8 Jul 2025 09:09:11 +0200 Subject: [PATCH 27/28] Refactor SemVer constraint to use non-nullable constructor arguments - Move default values from property declarations to constructor arguments - Make message, minMessage, maxMessage, and strict parameters non-nullable - Rearrange parameters in logical order: strict, message, min, minMessage, max, maxMessage --- .../Validator/Constraints/SemVer.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index 38caa6c3ca6cb..b0a9bab0fe685 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -39,10 +39,10 @@ class SemVer extends Constraint self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', ]; - public string $message = 'This value is not a valid semantic version.'; - public string $minMessage = 'This value should be {{ min }} or more.'; - public string $maxMessage = 'This value should be {{ max }} or less.'; - public bool $strict = true; + public string $message; + public string $minMessage; + public string $maxMessage; + public bool $strict; public ?string $min = null; public ?string $max = null; @@ -51,22 +51,22 @@ class SemVer extends Constraint */ #[HasNamedArguments] public function __construct( - ?string $message = null, - ?string $minMessage = null, - ?string $maxMessage = null, - ?bool $strict = null, + bool $strict = true, + string $message = 'This value is not a valid semantic version.', ?string $min = null, + string $minMessage = 'This value should be {{ min }} or more.', ?string $max = null, + string $maxMessage = 'This value should be {{ max }} or less.', ?array $groups = null, mixed $payload = null, ) { parent::__construct(null, $groups, $payload); - $this->message = $message ?? $this->message; - $this->minMessage = $minMessage ?? $this->minMessage; - $this->maxMessage = $maxMessage ?? $this->maxMessage; - $this->strict = $strict ?? $this->strict; + $this->strict = $strict; + $this->message = $message; $this->min = $min; + $this->minMessage = $minMessage; $this->max = $max; + $this->maxMessage = $maxMessage; } } From 09dbe1c3db9a2a0928adc8cfa306284dfc68fab4 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 8 Jul 2025 09:10:58 +0200 Subject: [PATCH 28/28] Fix parameter order: keep nullable parameters at the end Reorder constructor parameters to follow Symfony conventions where nullable parameters should come after non-nullable ones. --- .../Component/Validator/Constraints/SemVer.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/SemVer.php b/src/Symfony/Component/Validator/Constraints/SemVer.php index b0a9bab0fe685..ad253c27b50c0 100644 --- a/src/Symfony/Component/Validator/Constraints/SemVer.php +++ b/src/Symfony/Component/Validator/Constraints/SemVer.php @@ -51,22 +51,22 @@ class SemVer extends Constraint */ #[HasNamedArguments] public function __construct( - bool $strict = true, string $message = 'This value is not a valid semantic version.', - ?string $min = null, string $minMessage = 'This value should be {{ min }} or more.', - ?string $max = null, string $maxMessage = 'This value should be {{ max }} or less.', + bool $strict = true, + ?string $min = null, + ?string $max = null, ?array $groups = null, mixed $payload = null, ) { parent::__construct(null, $groups, $payload); - $this->strict = $strict; $this->message = $message; - $this->min = $min; $this->minMessage = $minMessage; - $this->max = $max; $this->maxMessage = $maxMessage; + $this->strict = $strict; + $this->min = $min; + $this->max = $max; } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy