Symfony, a leading PHP framework, is consistently updated to leverage modern PHP features. With PHP 8, attributes provide a new way to define metadata for classes, methods, properties, etc., which can be used for validation constraints. This blog post will guide you through creating and using a custom validator in Symfony to validate UK mobile number prefixes using PHP attributes. What Are PHP Attributes? PHP attributes, introduced in PHP 8, enable you to add metadata to various code elements, accessible via reflection. In Symfony, attributes can simplify defining validation constraints, making your code more concise and readable. Example 1 - Creating a Custom Validator for UK Mobile Number Prefix Let's create a custom validator to check if a phone number has a valid UK mobile prefix (e.g., starting with '07'). Step 1: Define the Attribute Create a new attribute class that defines the custom constraint. // src/Validator/Constraints/UkMobile.php namespace App\Validator\Constraints; use Attribute; use Symfony\Component\Validator\Constraint; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class UkMobile extends Constraint { public string $message = 'The number "{{ string }}" is not a valid UK mobile number.'; } Step 2: Create the Validator Next, create the validator with the logic to check if a phone number has a valid UK mobile prefix. // src/Validator/Constraints/UkMobileValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; class UkMobileValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (null === $value || '' === $value) { return; } if (!is_string($value)) { throw new UnexpectedTypeException($value, 'string'); } // Check if the number starts with '07' if (!preg_match('/^07[0-9]{9}$/', $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ string }}', $value) ->addViolation(); } } } Step 3: Apply the Attribute in an Entity Use the UkMobile attribute in your entities to enforce this custom validation rule. // src/Entity/User.php namespace App\Entity; use App\Validator\Constraints as AppAssert; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] class User { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private $id; #[ORM\Column(type: 'string', length: 15)] #[Assert\NotBlank] #[AppAssert\UkMobile] private $mobileNumber; // getters and setters } Step 4: Test the Validator Ensure everything works correctly by writing some unit tests or using Symfony's built-in validation mechanism. // tests/Validator/Constraints/UkMobileValidatorTest.php namespace App\Tests\Validator\Constraints; use App\Validator\Constraints\UkMobile; use App\Validator\Constraints\UkMobileValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class UkMobileValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): ConstraintValidator { return new UkMobileValidator(); } public function testNullIsValid(): void { $this->validator->validate(null, new UkMobile()); $this->assertNoViolation(); } public function testValidUkMobileNumber(): void { $this->validator->validate('07123456789', new UkMobile()); $this->assertNoViolation(); } public function testInvalidUkMobileNumber(): void { $constraint = new UkMobile(); $this->validator->validate('08123456789', $constraint); $this->buildViolation($constraint->message) ->setParameter('{{ string }}', '08123456789') ->assertRaised(); } } Example 2 - Creating a Custom Validator for Glasgow Postcodes In this example, we want to create a custom validator to check if a postcode is a valid Glasgow postcode. This could be used for professional trade services i.e. bark.com where a company only services certain areas. Step 1: Define the Attribute First, create a new attribute class to define the custom constraint. // src/Validator/Constraints/GlasgowPostcode.php namespace App\Validator\Constraints; use Attribute; use Symfony\Component\Validator\Constraint; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class GlasgowPostcode extends Constraint { public string $message = 'The postcode "{{ string }}" is not a valid Glasgow postcode.'; } Step 2: Create the Validator Next, create the validator with the logic to check if a postcode is a valid Glasgow postcode. // src/Validator/Constraints/GlasgowPostcodeValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; class GlasgowPostcodeValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (null === $value || '' === $value) { return; } if (!is_string($value)) { throw new UnexpectedTypeException($value, 'string'); } // Regex for validating Glasgow postcodes (starting with G) $pattern = '/^G\d{1,2}\s?\d[A-Z]{2}$/i'; if (!preg_match($pattern, $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ string }}', $value) ->addViolation(); } } } Step 3: Apply the Attribute in an Entity Use the GlasgowPostcode attribute in your entities to enforce this custom validation rule. // src/Entity/Address.php namespace App\Entity; use App\Validator\Constraints as AppAssert; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] class Address { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private $id; #[ORM\Column(type: 'string', length: 10)] #[Assert\NotBlank] #[AppAssert\GlasgowPostcode] private $postcode; // getters and setters } Step 4: Test the Validator Ensure everything works correctly by writing some unit tests or using Symfony's built-in validation mechanism. // tests/Validator/Constraints/GlasgowPostcodeValidatorTest.php namespace App\Tests\Validator\Constraints; use App\Validator\Constraints\GlasgowPostcode; use App\Validator\Constraints\GlasgowPostcodeValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class GlasgowPostcodeValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): ConstraintValidator { return new GlasgowPostcodeValidator(); } public function testNullIsValid(): void { $this->validator->validate(null, new GlasgowPostcode()); $this->assertNoViolation(); } public function testValidGlasgowPostcode(): void { $this->validator->validate('G1 1AA', new GlasgowPostcode()); $this->assertNoViolation(); } public function testInvalidGlasgowPostcode(): void { $constraint = new GlasgowPostcode(); $this->validator->validate('EH1 1AA', $constraint); $this->buildViolation($constraint->message) ->setParameter('{{ string }}', 'EH1 1AA') ->assertRaised(); } } Beyond Entities Custom validators aren't restricted to entities. They can be used to apply validation to properties and methods of any class you need, for example, if we wanted to use the GlasgowPostcode validator in a DTO object we could do something like // src/DTO/PostcodeDTO.php namespace App\DTO; use App\Validator\Constraints as AppAssert; use Symfony\Component\Validator\Constraints as Assert; class PostcodeDTO { #[Assert\NotBlank] #[AppAssert\GlasgowPostcode] private string $postcode; public function __construct(string $postcode) { $this->postcode = $postcode; } public function getPostcode(): string { return $this->postcode; } } and to check this DTO contains valid data we would make use of the validation service like $postcodeDTO = new PostcodeDTO('G1 1AA'); $violations = $this->validator->validate($postcodeDTO); Conclusion Using PHP attributes to define custom validators in Symfony can enhance code readability and leverage modern PHP features. Following the steps outlined above, you can create robust, reusable validation logic that integrates seamlessly with Symfony's validation system. This approach simplifies adding custom validations and keeps your code clean and maintainable. Happy coding! Originally published at https://chrisshennan.com/blog/using-php-attributes-to-create-and-use-a-custom-validator-in-symfony Symfony, a leading PHP framework, is consistently updated to leverage modern PHP features. With PHP 8, attributes provide a new way to define metadata for classes, methods, properties, etc., which can be used for validation constraints. This blog post will guide you through creating and using a custom validator in Symfony to validate UK mobile number prefixes using PHP attributes. What Are PHP Attributes? PHP attributes, introduced in PHP 8, enable you to add metadata to various code elements, accessible via reflection. In Symfony, attributes can simplify defining validation constraints, making your code more concise and readable. Example 1 - Creating a Custom Validator for UK Mobile Number Prefix Let's create a custom validator to check if a phone number has a valid UK mobile prefix (e.g., starting with '07'). Step 1: Define the Attribute Create a new attribute class that defines the custom constraint. // src/Validator/Constraints/UkMobile.php namespace App\Validator\Constraints; use Attribute; use Symfony\Component\Validator\Constraint; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class UkMobile extends Constraint { public string $message = 'The number "{{ string }}" is not a valid UK mobile number.'; } // src/Validator/Constraints/UkMobile.php namespace App\Validator\Constraints; use Attribute; use Symfony\Component\Validator\Constraint; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class UkMobile extends Constraint { public string $message = 'The number "{{ string }}" is not a valid UK mobile number.'; } Step 2: Create the Validator Next, create the validator with the logic to check if a phone number has a valid UK mobile prefix. // src/Validator/Constraints/UkMobileValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; class UkMobileValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (null === $value || '' === $value) { return; } if (!is_string($value)) { throw new UnexpectedTypeException($value, 'string'); } // Check if the number starts with '07' if (!preg_match('/^07[0-9]{9}$/', $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ string }}', $value) ->addViolation(); } } } // src/Validator/Constraints/UkMobileValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; class UkMobileValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (null === $value || '' === $value) { return; } if (!is_string($value)) { throw new UnexpectedTypeException($value, 'string'); } // Check if the number starts with '07' if (!preg_match('/^07[0-9]{9}$/', $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ string }}', $value) ->addViolation(); } } } Step 3: Apply the Attribute in an Entity Use the UkMobile attribute in your entities to enforce this custom validation rule. // src/Entity/User.php namespace App\Entity; use App\Validator\Constraints as AppAssert; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] class User { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private $id; #[ORM\Column(type: 'string', length: 15)] #[Assert\NotBlank] #[AppAssert\UkMobile] private $mobileNumber; // getters and setters } // src/Entity/User.php namespace App\Entity; use App\Validator\Constraints as AppAssert; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] class User { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private $id; #[ORM\Column(type: 'string', length: 15)] #[Assert\NotBlank] #[AppAssert\UkMobile] private $mobileNumber; // getters and setters } Step 4: Test the Validator Ensure everything works correctly by writing some unit tests or using Symfony's built-in validation mechanism. // tests/Validator/Constraints/UkMobileValidatorTest.php namespace App\Tests\Validator\Constraints; use App\Validator\Constraints\UkMobile; use App\Validator\Constraints\UkMobileValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class UkMobileValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): ConstraintValidator { return new UkMobileValidator(); } public function testNullIsValid(): void { $this->validator->validate(null, new UkMobile()); $this->assertNoViolation(); } public function testValidUkMobileNumber(): void { $this->validator->validate('07123456789', new UkMobile()); $this->assertNoViolation(); } public function testInvalidUkMobileNumber(): void { $constraint = new UkMobile(); $this->validator->validate('08123456789', $constraint); $this->buildViolation($constraint->message) ->setParameter('{{ string }}', '08123456789') ->assertRaised(); } } // tests/Validator/Constraints/UkMobileValidatorTest.php namespace App\Tests\Validator\Constraints; use App\Validator\Constraints\UkMobile; use App\Validator\Constraints\UkMobileValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class UkMobileValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): ConstraintValidator { return new UkMobileValidator(); } public function testNullIsValid(): void { $this->validator->validate(null, new UkMobile()); $this->assertNoViolation(); } public function testValidUkMobileNumber(): void { $this->validator->validate('07123456789', new UkMobile()); $this->assertNoViolation(); } public function testInvalidUkMobileNumber(): void { $constraint = new UkMobile(); $this->validator->validate('08123456789', $constraint); $this->buildViolation($constraint->message) ->setParameter('{{ string }}', '08123456789') ->assertRaised(); } } Example 2 - Creating a Custom Validator for Glasgow Postcodes In this example, we want to create a custom validator to check if a postcode is a valid Glasgow postcode. This could be used for professional trade services i.e. bark.com where a company only services certain areas. bark.com Step 1: Define the Attribute First, create a new attribute class to define the custom constraint. // src/Validator/Constraints/GlasgowPostcode.php namespace App\Validator\Constraints; use Attribute; use Symfony\Component\Validator\Constraint; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class GlasgowPostcode extends Constraint { public string $message = 'The postcode "{{ string }}" is not a valid Glasgow postcode.'; } // src/Validator/Constraints/GlasgowPostcode.php namespace App\Validator\Constraints; use Attribute; use Symfony\Component\Validator\Constraint; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class GlasgowPostcode extends Constraint { public string $message = 'The postcode "{{ string }}" is not a valid Glasgow postcode.'; } Step 2: Create the Validator Next, create the validator with the logic to check if a postcode is a valid Glasgow postcode. // src/Validator/Constraints/GlasgowPostcodeValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; class GlasgowPostcodeValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (null === $value || '' === $value) { return; } if (!is_string($value)) { throw new UnexpectedTypeException($value, 'string'); } // Regex for validating Glasgow postcodes (starting with G) $pattern = '/^G\d{1,2}\s?\d[A-Z]{2}$/i'; if (!preg_match($pattern, $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ string }}', $value) ->addViolation(); } } } // src/Validator/Constraints/GlasgowPostcodeValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; class GlasgowPostcodeValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (null === $value || '' === $value) { return; } if (!is_string($value)) { throw new UnexpectedTypeException($value, 'string'); } // Regex for validating Glasgow postcodes (starting with G) $pattern = '/^G\d{1,2}\s?\d[A-Z]{2}$/i'; if (!preg_match($pattern, $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ string }}', $value) ->addViolation(); } } } Step 3: Apply the Attribute in an Entity Use the GlasgowPostcode attribute in your entities to enforce this custom validation rule. // src/Entity/Address.php namespace App\Entity; use App\Validator\Constraints as AppAssert; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] class Address { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private $id; #[ORM\Column(type: 'string', length: 10)] #[Assert\NotBlank] #[AppAssert\GlasgowPostcode] private $postcode; // getters and setters } // src/Entity/Address.php namespace App\Entity; use App\Validator\Constraints as AppAssert; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] class Address { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private $id; #[ORM\Column(type: 'string', length: 10)] #[Assert\NotBlank] #[AppAssert\GlasgowPostcode] private $postcode; // getters and setters } Step 4: Test the Validator Ensure everything works correctly by writing some unit tests or using Symfony's built-in validation mechanism. // tests/Validator/Constraints/GlasgowPostcodeValidatorTest.php namespace App\Tests\Validator\Constraints; use App\Validator\Constraints\GlasgowPostcode; use App\Validator\Constraints\GlasgowPostcodeValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class GlasgowPostcodeValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): ConstraintValidator { return new GlasgowPostcodeValidator(); } public function testNullIsValid(): void { $this->validator->validate(null, new GlasgowPostcode()); $this->assertNoViolation(); } public function testValidGlasgowPostcode(): void { $this->validator->validate('G1 1AA', new GlasgowPostcode()); $this->assertNoViolation(); } public function testInvalidGlasgowPostcode(): void { $constraint = new GlasgowPostcode(); $this->validator->validate('EH1 1AA', $constraint); $this->buildViolation($constraint->message) ->setParameter('{{ string }}', 'EH1 1AA') ->assertRaised(); } } // tests/Validator/Constraints/GlasgowPostcodeValidatorTest.php namespace App\Tests\Validator\Constraints; use App\Validator\Constraints\GlasgowPostcode; use App\Validator\Constraints\GlasgowPostcodeValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class GlasgowPostcodeValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): ConstraintValidator { return new GlasgowPostcodeValidator(); } public function testNullIsValid(): void { $this->validator->validate(null, new GlasgowPostcode()); $this->assertNoViolation(); } public function testValidGlasgowPostcode(): void { $this->validator->validate('G1 1AA', new GlasgowPostcode()); $this->assertNoViolation(); } public function testInvalidGlasgowPostcode(): void { $constraint = new GlasgowPostcode(); $this->validator->validate('EH1 1AA', $constraint); $this->buildViolation($constraint->message) ->setParameter('{{ string }}', 'EH1 1AA') ->assertRaised(); } } Beyond Entities Custom validators aren't restricted to entities. They can be used to apply validation to properties and methods of any class you need, for example, if we wanted to use the GlasgowPostcode validator in a DTO object we could do something like // src/DTO/PostcodeDTO.php namespace App\DTO; use App\Validator\Constraints as AppAssert; use Symfony\Component\Validator\Constraints as Assert; class PostcodeDTO { #[Assert\NotBlank] #[AppAssert\GlasgowPostcode] private string $postcode; public function __construct(string $postcode) { $this->postcode = $postcode; } public function getPostcode(): string { return $this->postcode; } } // src/DTO/PostcodeDTO.php namespace App\DTO; use App\Validator\Constraints as AppAssert; use Symfony\Component\Validator\Constraints as Assert; class PostcodeDTO { #[Assert\NotBlank] #[AppAssert\GlasgowPostcode] private string $postcode; public function __construct(string $postcode) { $this->postcode = $postcode; } public function getPostcode(): string { return $this->postcode; } } and to check this DTO contains valid data we would make use of the validation service like $postcodeDTO = new PostcodeDTO('G1 1AA'); $violations = $this->validator->validate($postcodeDTO); $postcodeDTO = new PostcodeDTO('G1 1AA'); $violations = $this->validator->validate($postcodeDTO); Conclusion Using PHP attributes to define custom validators in Symfony can enhance code readability and leverage modern PHP features. Following the steps outlined above, you can create robust, reusable validation logic that integrates seamlessly with Symfony's validation system. This approach simplifies adding custom validations and keeps your code clean and maintainable. Happy coding! Originally published at https://chrisshennan.com/blog/using-php-attributes-to-create-and-use-a-custom-validator-in-symfony Originally published at https://chrisshennan.com/blog/using-php-attributes-to-create-and-use-a-custom-validator-in-symfony https://chrisshennan.com/blog/using-php-attributes-to-create-and-use-a-custom-validator-in-symfony