Symfony 7.4: Extending Validation and Serialization with PHP Attributes

symfony

Explore how Symfony 7.4 simplifies customizing validation and serialization metadata for external classes using PHP attributes. Learn to extend existing definitions without XML or YAML configuration.

Previously, Symfony applications that relied on external bundles and packages often faced challenges when customizing validation or serialization for their classes. This typically required redefining metadata in XML or YAML files, placed in specific configuration directories like config/validation/, which often felt disconnected from the actual application code.

Symfony 7.4 introduces a more integrated approach, allowing developers to extend validation and serialization metadata using PHP attributes.

To extend the validation metadata of an external class, create a new class anywhere in your application and apply the #[ExtendsValidationFor] attribute. This attribute declares the Fully Qualified Class Name (FQCN) of the class you intend to extend:

use Acme\Some\Bundle\UserRegistration;
use Symfony\Component\Validator\Attribute\ExtendsValidationFor;

#[ExtendsValidationFor(UserRegistration::class)]
class UserRegistrationValidation
{
    // ...
}

Your extension class can be named as you prefer. Inside it, add the properties and getters you wish to extend from the original class, ensuring they use the exact same names:

use Symfony\Component\Validator\Attribute\ExtendsValidationFor;
use Symfony\Component\Validator\Constraints as Assert;

#[ExtendsValidationFor(UserRegistration::class)]
class UserRegistrationValidation
{
    #[Assert\NotBlank(groups: ['my_app'])]
    #[Assert\Length(min: 3, groups: ['my_app'])]
    public string $name = '';

    #[Assert\Email(groups: ['my_app'])]
    public string $email = '';

    #[Assert\Range(min: 18, groups: ['my_app'])]
    public int $age = 0;
}

How this works:

  • During container compilation, Symfony collects classes marked with #[ExtendsValidationFor(Target::class)] and verifies that the properties and getters declared in your extension class exist in the target class. If a mismatch is found, a MappingException is thrown.
  • The validator is configured to map the target class to your validation extension class.
  • At runtime, when loading validation metadata for the target class, Symfony reads attributes (constraints, callbacks, group providers) from both the original class and your extension class, merging them together.

These extension classes are not intended for instantiation. If you prefer, you can declare them as abstract:

#[ExtendsValidationFor(UserRegistration::class)]
abstract class UserRegistrationValidation
{
    // ...
}

Symfony 7.4 applies the same concept to serialization metadata. By using the #[ExtendsSerializationFor] attribute on your own classes, you can declare new serialization attributes for third-party classes, eliminating the need for XML or YAML configuration in the config/serialization/ directory:

use Symfony\Component\Serializer\Attribute\ExtendsSerializationFor;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\MaxDepth;
use Symfony\Component\Serializer\Attribute\SerializedName;

// ...
#[ExtendsSerializationFor(UserRegistration::class)]
abstract class UserRegistrationSerialization
{
    #[Groups(['my_app'])]
    #[SerializedName('fullName')]
    public string $name = '';

    #[Groups(['my_app'])]
    public string $email = '';

    #[Groups(['my_app'])]
    #[MaxDepth(2)]
    public Category $category;
}

Similar to validation, Symfony verifies that properties or accessors declared in your extension class exist in the target class. At runtime, metadata from both the original class and your extension class is merged.