diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 6e65a1355fdaf..cf1358e94de9e 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.1 --- + * Add `MacAddress` constraint * Add `list` and `associative_array` types to `Type` constraint * Add the `Charset` constraint diff --git a/src/Symfony/Component/Validator/Constraints/MacAddress.php b/src/Symfony/Component/Validator/Constraints/MacAddress.php new file mode 100644 index 0000000000000..fda431d792235 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/MacAddress.php @@ -0,0 +1,52 @@ + + * + * 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\Exception\InvalidArgumentException; + +/** + * Validates that a value is a valid MAC address. + * + * @author Ninos Ego + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class MacAddress extends Constraint +{ + public const INVALID_MAC_ERROR = 'a183fbff-6968-43b4-82a2-cc5cf7150036'; + + protected const ERROR_NAMES = [ + self::INVALID_MAC_ERROR => 'INVALID_MAC_ERROR', + ]; + + public string $message = 'This is not a valid MAC address.'; + + /** @var callable|null */ + public $normalizer; + + public function __construct( + array $options = null, + string $message = null, + callable $normalizer = null, + array $groups = null, + mixed $payload = null, + ) { + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->normalizer = $normalizer ?? $this->normalizer; + + if (null !== $this->normalizer && !\is_callable($this->normalizer)) { + throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); + } + } +} diff --git a/src/Symfony/Component/Validator/Constraints/MacAddressValidator.php b/src/Symfony/Component/Validator/Constraints/MacAddressValidator.php new file mode 100644 index 0000000000000..a93f90b4d877f --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/MacAddressValidator.php @@ -0,0 +1,53 @@ + + * + * 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; + +/** + * Validates whether a value is a valid MAC address. + * + * @author Ninos Ego + */ +class MacAddressValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof MacAddress) { + throw new UnexpectedTypeException($constraint, MacAddress::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = (string) $value; + + if (null !== $constraint->normalizer) { + $value = ($constraint->normalizer)($value); + } + + if (!filter_var($value, \FILTER_VALIDATE_MAC)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->addViolation(); + } + } +} diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf index 50b8874d519a5..f94bb11eef572 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf @@ -434,6 +434,10 @@ The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}. Der erkannte Zeichensatz ist nicht gültig ({{ detected }}). Gültige Zeichensätze sind "{{ encodings }}". + + This is not a valid MAC address. + Dies ist keine gültige MAC-Adresse. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf index 6a49fb39f627d..0fe425b20c60f 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf @@ -434,6 +434,10 @@ The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}. The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}. + + This is not a valid MAC address. + This is not a valid MAC address. + diff --git a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressTest.php b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressTest.php new file mode 100644 index 0000000000000..d39180bc09f74 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressTest.php @@ -0,0 +1,70 @@ + + * + * 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 PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\MacAddress; +use Symfony\Component\Validator\Exception\InvalidArgumentException; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; + +/** + * @author Ninos Ego + */ +class MacAddressTest extends TestCase +{ + public function testNormalizerCanBeSet() + { + $mac = new MacAddress(['normalizer' => 'trim']); + + $this->assertEquals('trim', $mac->normalizer); + } + + public function testInvalidNormalizerThrowsException() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "normalizer" option must be a valid callable ("string" given).'); + new MacAddress(['normalizer' => 'Unknown Callable']); + } + + public function testInvalidNormalizerObjectThrowsException() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "normalizer" option must be a valid callable ("stdClass" given).'); + new MacAddress(['normalizer' => new \stdClass()]); + } + + public function testAttributes() + { + $metadata = new ClassMetadata(MacAddressDummy::class); + $loader = new AttributeLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + self::assertSame('myMessage', $aConstraint->message); + self::assertSame('trim', $aConstraint->normalizer); + self::assertSame(['Default', 'MacAddressDummy'], $aConstraint->groups); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + self::assertSame(['my_group'], $bConstraint->groups); + self::assertSame('some attached data', $bConstraint->payload); + } +} + +class MacAddressDummy +{ + #[MacAddress(message: 'myMessage', normalizer: 'trim')] + private $a; + + #[MacAddress(groups: ['my_group'], payload: 'some attached data')] + private $b; +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php new file mode 100644 index 0000000000000..0c56bcd23d210 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php @@ -0,0 +1,125 @@ + + * + * 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\MacAddress; +use Symfony\Component\Validator\Constraints\MacAddressValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +/** + * @author Ninos Ego + */ +class MacAddressValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): MacAddressValidator + { + return new MacAddressValidator(); + } + + public function testNullIsValid() + { + $this->validator->validate(null, new MacAddress()); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid() + { + $this->validator->validate('', new MacAddress()); + + $this->assertNoViolation(); + } + + public function testExpectsStringCompatibleType() + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new \stdClass(), new MacAddress()); + } + + /** + * @dataProvider getValidMacs + */ + public function testValidMac($mac) + { + $this->validator->validate($mac, new MacAddress()); + + $this->assertNoViolation(); + } + + public static function getValidMacs(): array + { + return [ + ['00:00:00:00:00:00'], + ['00-00-00-00-00-00'], + ['ff:ff:ff:ff:ff:ff'], + ['ff-ff-ff-ff-ff-ff'], + ['FF:FF:FF:FF:FF:FF'], + ['FF-FF-FF-FF-FF-FF'], + ]; + } + + /** + * @dataProvider getValidMacsWithWhitespaces + */ + public function testValidMacsWithWhitespaces($mac) + { + $this->validator->validate($mac, new MacAddress([ + 'normalizer' => 'trim', + ])); + + $this->assertNoViolation(); + } + + public static function getValidMacsWithWhitespaces(): array + { + return [ + ["\x2000:00:00:00:00:00"], + ["\x09\x0900-00-00-00-00-00"], + ["ff:ff:ff:ff:ff:ff\x0A"], + ["ff-ff-ff-ff-ff-ff\x0D\x0D"], + ["\x00FF:FF:FF:FF:FF:FF\x00"], + ["\x0B\x0BFF-FF-FF-FF-FF-FF\x0B\x0B"], + ]; + } + + /** + * @dataProvider getInvalidMacs + */ + public function testInvalidMacs($mac) + { + $constraint = new MacAddress([ + 'message' => 'myMessage', + ]); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + public static function getInvalidMacs(): array + { + return [ + ['0'], + ['00:00'], + ['00:00:00'], + ['00:00:00:00'], + ['00:00:00:00:00'], + ['00:00:00:00:00:000'], + ['-00:00:00:00:00:00'], + ['foobar'], + ]; + } +}