Skip to content

Conversation

nicolas-grekas
Copy link
Member

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

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

Prerequisite for #61288

At the moment, validation attributes are read at runtime when framework.validation.enable_attributes is true.

This means they don't fit for bundles nor can't they be warmed up.

This PR fixes both issues by using a new validator.attribute_metadata resource tag, that's turned into a list of classes to parse for attributes at compile-time.

For apps, the tag is added by autoconfiguration: any Constraint-derived attributes found on a class in the src/ folder will trigger the rule to add the tag.

For bundles (and for apps if they want to), the tag is added by explicit service configuration. In an "eat your own dog-food" spirit, this capability is used to declare the constraints of the Form class: instead of loading the validation.xml file, we now declare this service resource:

        ->set('validator.form.attribute_metadata', Form::class)
            ->tag('container.excluded')
            ->tag('validator.attribute_metadata')

This reads the attributes added to the Form class:

#[AssertForm()]
#[Traverse(false)]
class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterface

Bundles can do the same and replace their XML files by attributes.

As a next step, we could also deprecate runtime-discovery of attributes. This could be worth it if this discovery has a measurable performance impact. To be measured if one wants to dig this idea.

Side note: I'm hoping this could allow removing the yaml and xml config formats one day. For serialization metadata also (PR coming). BUT, this doesn't (yet) cover the use case of overriding metadata defined by bundles. For that, apps still have to use xml or yaml in config/validation/. I have an idea to cover this, coming to a next PR if it works.

(failures unrelated)

@carsonbot carsonbot added this to the 7.4 milestone Aug 26, 2025
@nicolas-grekas nicolas-grekas force-pushed the validator.attribute_metadata branch 3 times, most recently from 289783f to 8be45c5 Compare August 26, 2025 08:37
@nicolas-grekas nicolas-grekas changed the title [Validator] Allow using attributes on classes as constraint metadata configuration [Validator] Allow using attributes to declare compile-time constraint metadata Aug 26, 2025
@nicolas-grekas nicolas-grekas force-pushed the validator.attribute_metadata branch 3 times, most recently from defd051 to ea317e8 Compare August 26, 2025 16:01
@nicolas-grekas nicolas-grekas force-pushed the validator.attribute_metadata branch 2 times, most recently from d3490c8 to 9d08608 Compare August 27, 2025 10:05
@nicolas-grekas nicolas-grekas force-pushed the validator.attribute_metadata branch 2 times, most recently from 3dc6ebf to 35c205e Compare August 27, 2025 12:41
@nicolas-grekas nicolas-grekas force-pushed the validator.attribute_metadata branch from 35c205e to 325c0c3 Compare August 27, 2025 12:59
if (!ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/validator'])) {
$container->removeDefinition('validator.form.attribute_metadata');
} elseif (!($r = new \ReflectionClass(Form::class))->getAttributes(Traverse::class) || !class_exists(ValidatorAttributeMetadataPass::class)) {
$fileRecorder('xml', \dirname($r->getFileName()).'/Resources/config/validation.xml');
Copy link
Member

Choose a reason for hiding this comment

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

I suggest adding a comment that this is for symfony/form < 7.4 or symfony/validator < 7.4, to make it easier to clean this code when bumping the min version of those packages supported in FrameworkBundle (which will likely happen at some point during the 8.x lifecycle)

@@ -27,6 +27,7 @@
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
Copy link
Member

Choose a reason for hiding this comment

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

Why adding this here ? The abstract class cannot be used as an attribute. And child classes defining concrete constraints will have to specify the proper target anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's required by the engine in relation to registerAttributeForAutoconfiguration

*/
private function extractSupportedLoaders(array $loaders): array
{
$supportedLoaders = [];

foreach ($loaders as $loader) {
if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) {
if (method_exists($loader, 'getMappedClasses')) {
Copy link
Member

Choose a reason for hiding this comment

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

This method existence should be checked using an interface. For compatibility with older versions of the validator component, we can keep checking the concrete classes.

Suggested change
if (method_exists($loader, 'getMappedClasses')) {
if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader || $loader instanceof WarmableLoaderInterface)

Comment on lines 90 to 91
} elseif ($loader instanceof LoaderChain) {
$supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getLoaders()));
Copy link
Member

Choose a reason for hiding this comment

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

The LoaderChain can implement the getMappedClasses method, so that we can remove this specific case in 8.0, once validator 7.4+ is required.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think all this is pretty much internal, I'm not sure adding an interface and the rest is worth it.

Copy link
Member

Choose a reason for hiding this comment

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

This makes the intention clearer and the code more understandable and statically analyzable.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd really prefer not opening a new abstraction. Nobody asked for any while this code exists since years...

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