-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Validator] Constraints as php 8 Attributes #38309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
d271b9e
to
3b6c091
Compare
853b243
to
72a16c0
Compare
this is a pure implementation concern, isn't it? that's fine to me then.
This would be the best IMHO. Any idea? Here is one that could be compatible with IDE autocompletion:
Dunno how this would work technically, but I feel like it could. Any better idea? |
Another idea, similar to how the config component works (indentation would be "manual"): #[Assert\All]
#[Assert\NotNull]
#[Assert\Length(max: 6)]
#[EndAssert\All] |
I prefer the previous recommendation based on Given that PHP attributes use the exact same syntax that Rust has been using for years, we could look into them for inspiration. A random example taken from this library: struct SignupData {
#[validate(email)]
mail: String,
#[validate(phone)]
phone: String,
#[validate(url)]
site: String,
#[validate(length(min = 1), custom = "validate_unique_username")]
#[serde(rename = "firstName")]
first_name: String,
#[validate(range(min = 18, max = 20))]
age: u32,
} |
So, we would basically have dedicated start/end attributes as a bracket around nested attributes. And the annotation loader would understand how to transform a series of attributes to the expected nested structure. That'll work, I guess, though I would not reuse an existing attribute as start marker. Otherwise things might get complicated if one day php really implements nested attributes. Regarding the code-style: You can combine multiple attributes into one #[Assert\BeginAll,
AssertNotNull,
Assert\Length(max: 6),
Assert\EndAll]
#[SomeOtherConstraint]
#[SomeUnrelatedAttribute] This is of course just a visual aid. There is no way to query for the individual attribute blocks via reflection. Another idea that I had was to define an array structure that can be converted easily to a constructor call. #[Assert\All(constraints: [
[Assert\NotNull::class],
[Assert\Length::class, 'max' => 6],
])] We could provide a helper function (via a trait or the abstract |
not convinced at all. I like your idea of tricking the syntax to emulate nested attributes. #[Assert\All,
AssertNotNull,
Assert\Length(max: 6),
EndAssert] Such a list could be interpreted quite generically I think, from an implementation pov; |
Not having a distinct starter for a nested block makes the implementation more difficult, but I think we can do without. The generic |
Here are some other proposals to flatten the nested attributes. Taking this example from Symfony Docs: class User
{
/**
* @Assert\All({
* @Assert\NotBlank,
* @Assert\Length(min=5)
* })
*/
protected $favoriteColors = [];
} We could add a "special marker" in the attribute name to say that this only applies to items (the idea would be to get this "marker" somehow and turn it into a config option ... not duplicating all constraint for items): class User
{
#[Assert\Items\NotBlank]
#[Assert\Items\Length(min: 5)]
protected $favoriteColors = [];
} Another idea: add an option which tells Symfony if this applies to the property or to each of its elements: class User
{
#[Assert\NotBlank, apply_to: items]
#[Assert\Length(min: 5), apply_to: items]
protected $favoriteColors = [];
} |
@javiereguiluz I'm not sure how this would work: how would one tell they want either |
Before this PR escalates into a discussion on nesting: Reviews of the actual code of this PR would be very much appreciated. 😃 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me. Do you want to merge this first and iterate?
Just don't miss adding an entry in the changelog.
72a16c0
to
522ec73
Compare
Yes, please. This way, others could join in and migrate more constraints while I work on the nesting.
Added! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (cosmetic comments only)
522ec73
to
b71b352
Compare
5785e93
to
4d06b99
Compare
4d06b99
to
61554e3
Compare
61554e3
to
d1cb2d6
Compare
Thank you @derrabus. |
…t doctrine/annotations (derrabus) This PR was merged into the 5.2-dev branch. Discussion ---------- [Validator] Allow load mappings from attributes without doctrine/annotations | Q | A | ------------- | --- | Branch? | 5.2 | Bug fix? | no | New feature? | yes | Deprecations? | yes | Tickets | #38096 | License | MIT | Doc PR | TODO Follows #38309. Currently, we cannot enable constraint mapping from attributes without having `doctrine/annotations` installed. Lifting that limitation is a bit tricky because `ValidatorBuilder::enableAnnotationMapping()` creates an annotation reader if you don't pass one. This PR aims at deprecating this behavior. I know it's a bit late for such a change in 5.2 and I should have seen earlier that this part was missing. 😓 Since I don't expect people to go all-in on attributes on day one, it's probably okay to postpone this change to 5.3. Commits ------- 441c806 [Validator] Allow load mappings from attributes without doctrine/annotations.
…wkania) This PR was squashed before being merged into the 5.2 branch. Discussion ---------- [Validator] Document constraints as php 8 Attributes We already have [examples](https://github.com/symfony/symfony-docs/pull/14230/files) of code for the PHP attributes. We also have [Constraints as php 8 Attributes](symfony/symfony#38309) and [2](#14305). The rest will be implemented in the [future](symfony/symfony#38503). Commits ------- 748bd54 [Validator] Document constraints as php 8 Attributes
if (\is_array($choices) && \is_string(key($choices))) { | ||
$options = array_merge($choices, $options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@derrabus I often use associative arrays as choices - using the translation key as choice key ; one reason would be to share this constant array with the ChoiceType
choices
option. It was completely ok with Annotations. But this compatibility rule for the new Attribute usage is now concidering my choices keys as constraint options and breaks my code. I get that this is required to support both Annotation and Attribute but this changes the way this constraint is working.
// stupid example
abstract class FormatEnum
{
public const ALL = [
'enum.format.example1' => 'f1',
'enum.format.example2' => 'd6',
'enum.format.example3' => 'u9',
];
}
// Annotation: 👍🏻
// @Choice(choices=FormatEnum::ALL)
// Attribute: 👎🏻
// #[Choice(choices: FormatEnum::ALL)]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please open a bug for your issue?
This is my attempt to teach the validator to load constraints from PHP attributes. Like we've done it for the
Route
attribute, I've hooked into the existingAnnotationLoader
, so we can again mix and match annotations and attributes.Named Arguments
An attribute declaration is basically a constructor call. This is why, in order to effectively use a constraint as attribute, we need to equip it with a constructor that works nicely with named arguments. This way, IDEs like PhpStorm can provide auto-completion and guide a developer when declaring a constraint without the need for additional plugins. Right now, PhpStorm supports neither attributes nor named arguments, but I expect those features to be implemented relatively soon.
To showcase this, I have migrated the
Range
andChoice
constraints. The example presented in #38096 works with this PR.A nice side effect is that we get a more decent constructor call in php 8 in situations where we directly instantiate constraints, for instance when attaching constraints to a form field via the form builder.
The downside is that all those constructors generate the need for some boilerplate code that was previously abstracted away by the
Constraint
class.The alternative to named arguments would be leaving the constructors as they are. That would basically mean that when declaring a constraint we would have to emulate the array that Doctrine annotations would crate. We would lose IDE support and the declarations would be uglier, but it would work.
Nested Attributes
PHP does not support nesting attributes (yet?). This is why I could not recreate composite annotations like
All
andCollection
. I think it's okay if they're not included in the first iteration of attribute constraints and we can work on a solution in a later PR. Possible options:Repeatable attributes
In order to attach two instances of the same attribute class to an element, we explicitly have to allow repetition via the flag
Attribute::IS_REPEATABLE
. While we could argue if it really makes sense to do this for certain constraints (likeNotNull
for instance), there are others (likeCallback
) where having two instances with different configurations could make sense. On the other hand, the validator certainly won't break if we repeat any of the constraints and all other ways to configure constraints allow repetition. This is why I decided to allow repetition for all constraints I've marked as attributes in this PR and propose to continue with that practice for all other constraints.Migration Path
This PR only migrates a handful of constraints. My plan is to discuss the general idea with this PR first and use it as a blueprint to migrate the individual constraints afterwards. Right now, the migration path would look like this:
#[Attribute]
attribute.