Nicolas Grekas
Contributed by Nicolas Grekas in #61532 , #61545 and #61563

Many Symfony applications rely on external bundles and packages that provide their own classes. When you needed to customize how those classes were validated or serialized, you had to redefine their metadata in XML or YAML files placed in hardcoded configuration directories such as config/validation/. This worked, but it felt disconnected from your application code.

Symfony 7.4 introduces a better approach. You can now 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 FQCN of the class you want to extend:

1
2
3
4
5
6
7
8
use Acme\Some\Bundle\UserRegistration;
use Symfony\Component\Validator\Attribute\ExtendsValidationFor;

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

Your class can be named however you like. Inside it, add the properties and getters that you want to extend from the original class. Use the exact same names as in the original class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

  1. During container compilation, Symfony collects classes marked with #[ExtendsValidationFor(Target::class)] and verifies that the properties and getters declared in your class exist in the target class. If not, a MappingException is thrown.
  2. The validator is configured so that the target class is mapped to your validation extension class.
  3. 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 and merges them.

These extension classes are not meant to be instantiated. If you prefer, you can declare them as abstract:

1
2
3
4
5
#[ExtendsValidationFor(UserRegistration::class)]
abstract class UserRegistrationValidation
{
    // ...
}

Symfony 7.4 introduces the same idea for serialization metadata. By applying #[ExtendsSerializationFor] to your own classes, you can declare new serialization attributes for third party classes without defining XML or YAML in the hardcoded config/serialization/ directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}

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

Published in #Living on the edge