Skip to content

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Aug 28, 2025

Q A
Branch? 7.4
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

This PR builds on #61528

I propose to add a #[ValidationFor] attribute that allows adding validation constraints to another class.
This is typically needed for third party classes. For context, Sylius has a nice doc about this:
https://docs.sylius.com/the-customization-guide/customizing-validation

At the moment, the only way to achieve this is by declaring the new constraints in the (hardcoded) config/validation/ folder, using either xml or yaml. No attributes.

With this PR, one will be able to define those extra constraints using PHP attributes, set on classes that'd mirror the properties/getters of the targeted class. The compiler pass will ensure that all properties/getters declared in these source classes also exist in the target class. (source = the app's class that declares the new constraints; target = the existing class to add constraints to.)

#[ValidationFor(TargetClass::class)]
class SourceClass
{
    #[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;
}

Here are the basics of how this works:

  1. During container compilation, classes marked with #[ValidationFor(Target::class)] are collected and validated: the container checks that members declared on the source exist on the target. If not, a MappingException is thrown.
  2. The validator is configured to map the target to its source classes.
  3. At runtime, when loading validation metadata for the target, attributes (constraints, callbacks, group providers) are read from both the target and its mapped source classes and applied accordingly.

@alexandre-daubois
Copy link
Member

The feature seems nice, but I'm still trying to grasp what it really does. If I understand correctly and if we take Sylius example, is the following code correct?

use Sylius\Component\Product\Model\ProductTranslation;

#[ValidationFor(ProductTranslation::class)]
class MyProductTranslation
{
    #[Assert\NotBlank(groups: ['my_app'])] 
    #[Assert\Length(min: 10, groups: ['my_app'])]
    public string $name = '';
}

Then on validation, Sylius' ProductTranslation constraints would be overridden by the ones defined in MyProductTranslation. Is that right?

@nicolas-grekas
Copy link
Member Author

@alexandre-daubois you've got it right, provided you also configure the sylius.form.type.product_translation.validation_groups parameter as explained in the doc.
That's already doable using yaml and xml but not using plain PHP attributes.

@OskarStark OskarStark changed the title [Validator] Add #[ValidationFor] to declare new constraints for a class [Validator] Add #[ValidationFor] to declare new constraints for a class Aug 29, 2025
@OskarStark
Copy link
Contributor

Could we find a proper tribute name with something like #[As*]?

@nicolas-grekas
Copy link
Member Author

@OskarStark that's not a service. As* is for services to me.

Copy link
Member

@alexandre-daubois alexandre-daubois left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with class constraints? I don't think I saw support or tests for them. I wonder if they should be supported for feature completeness?

final class ValidationFor
{
public function __construct(
public string $class,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be documented as class-string

$mappedClasses[$resolve($definition->getClass())] = true;
$class = $resolve($definition->getClass());
foreach ($definition->getTag('validator.attribute_metadata') as $attributes) {
if ($class !== $for = isset($attributes['for']) ? $resolve($attributes['for']) : $class) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to support DI parameters in the for attribute ?

@nicolas-grekas
Copy link
Member Author

Thank you @alexandre-daubois and @stof, I addressed your comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants