Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5eb442e

Browse files
committed
feature #38309 [Validator] Constraints as php 8 Attributes (derrabus)
This PR was merged into the 5.2-dev branch. Discussion ---------- [Validator] Constraints as php 8 Attributes | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | #38096 | License | MIT | Doc PR | TODO 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 existing `AnnotationLoader`, 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` and `Choice` constraints. The example presented in #38096 works with this PR. ```php #[Assert\Choice( choices: ['fiction', 'non-fiction'], message: 'Choose a valid genre.', )] private $genre; ``` 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. ```php $builder->add('genre, TextType::class, [ 'constraints' => [new Assert\Choice( choices: ['fiction', 'non-fiction'], message: 'Choose a valid genre.', )], ]); ``` 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. ```php #[Assert\Choice([ 'choices' => ['fiction', 'non-fiction'], 'message' => 'Choose a valid genre.', ])] private $genre; ``` ### Nested Attributes PHP does not support nesting attributes (yet?). This is why I could not recreate composite annotations like `All` and `Collection`. 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: * A later PHP 8.x release might give us nested attributes. * We could find a way to flatten those constraints so we can recreate them without nesting. * We could come up with a convention for a structure that lets us emulate nested attributes in userland. ### 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 (like `NotNull` for instance), there are others (like `Callback`) 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: * Attach the `#[Attribute]` attribute. * Recreate all options of the constraint as constructor arguments. * Add test cases for constructor calls with named arguments to the test class of the constraint's validator. Commits ------- d1cb2d6 [Validator] Constraints as php 8 Attributes.
2 parents dbedd28 + d1cb2d6 commit 5eb442e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+955
-197
lines changed

.github/patch-types.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
case false !== strpos($file, '/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures'):
3737
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'):
3838
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/LotsOfAttributes.php'):
39+
case false !== strpos($file, '/src/Symfony/Component/Validator/Tests/Fixtures/Attribute/'):
3940
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/MyAttribute.php'):
4041
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'):
4142
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/Php74.php') && \PHP_VERSION_ID < 70400:

src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/Foo.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111

1212
namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute;
1313

14-
use Attribute;
1514
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
1615

17-
#[Attribute(Attribute::TARGET_PARAMETER)]
16+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
1817
class Foo implements ArgumentInterface
1918
{
2019
private $foo;

src/Symfony/Component/Routing/Annotation/Route.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
namespace Symfony\Component\Routing\Annotation;
1313

14-
use Attribute;
15-
1614
/**
1715
* Annotation class for @Route().
1816
*
@@ -22,7 +20,7 @@
2220
* @author Fabien Potencier <[email protected]>
2321
* @author Alexander M. Turek <[email protected]>
2422
*/
25-
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
23+
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
2624
class Route
2725
{
2826
private $path;

src/Symfony/Component/Security/Http/Attribute/CurrentUser.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111

1212
namespace Symfony\Component\Security\Http\Attribute;
1313

14-
use Attribute;
1514
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
1615

1716
/**
1817
* Indicates that a controller argument should receive the current logged user.
1918
*/
20-
#[Attribute(Attribute::TARGET_PARAMETER)]
19+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
2120
class CurrentUser implements ArgumentInterface
2221
{
2322
}

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ CHANGELOG
3232
* added the `Isin` constraint and validator
3333
* added the `ULID` constraint and validator
3434
* added support for UUIDv6 in `Uuid` constraint
35+
* enabled the validator to load constraints from PHP attributes
3536

3637
5.1.0
3738
-----

src/Symfony/Component/Validator/Constraint.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,11 @@ public static function getErrorName($errorCode)
9191
* getRequiredOptions() to return the names of these options. If any
9292
* option is not set here, an exception is thrown.
9393
*
94-
* @param mixed $options The options (as associative array)
95-
* or the value for the default
96-
* option (any other type)
94+
* @param mixed $options The options (as associative array)
95+
* or the value for the default
96+
* option (any other type)
97+
* @param string[] $groups An array of validation groups
98+
* @param mixed $payload Domain-specific data attached to a constraint
9799
*
98100
* @throws InvalidOptionsException When you pass the names of non-existing
99101
* options
@@ -103,9 +105,15 @@ public static function getErrorName($errorCode)
103105
* array, but getDefaultOption() returns
104106
* null
105107
*/
106-
public function __construct($options = null)
108+
public function __construct($options = null, array $groups = null, $payload = null)
107109
{
108-
foreach ($this->normalizeOptions($options) as $name => $value) {
110+
$options = $this->normalizeOptions($options);
111+
if (null !== $groups) {
112+
$options['groups'] = $groups;
113+
}
114+
$options['payload'] = $payload ?? $options['payload'] ?? null;
115+
116+
foreach ($options as $name => $value) {
109117
$this->$name = $value;
110118
}
111119
}

src/Symfony/Component/Validator/Constraints/Blank.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <[email protected]>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2223
class Blank extends Constraint
2324
{
2425
const NOT_BLANK_ERROR = '183ad2de-533d-4796-a439-6d3c3852b549';
@@ -28,4 +29,11 @@ class Blank extends Constraint
2829
];
2930

3031
public $message = 'This value should be blank.';
32+
33+
public function __construct(array $options = null, string $message = null, array $groups = null, $payload = null)
34+
{
35+
parent::__construct($options ?? [], $groups, $payload);
36+
37+
$this->message = $message ?? $this->message;
38+
}
3139
}

src/Symfony/Component/Validator/Constraints/Callback.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <[email protected]>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2223
class Callback extends Constraint
2324
{
2425
/**
@@ -28,19 +29,23 @@ class Callback extends Constraint
2829

2930
/**
3031
* {@inheritdoc}
32+
*
33+
* @param array|string|callable $callback The callback or a set of options
3134
*/
32-
public function __construct($options = null)
35+
public function __construct($callback = null, array $groups = null, $payload = null, array $options = [])
3336
{
3437
// Invocation through annotations with an array parameter only
35-
if (\is_array($options) && 1 === \count($options) && isset($options['value'])) {
36-
$options = $options['value'];
38+
if (\is_array($callback) && 1 === \count($callback) && isset($callback['value'])) {
39+
$callback = $callback['value'];
3740
}
3841

39-
if (\is_array($options) && !isset($options['callback']) && !isset($options['groups']) && !isset($options['payload'])) {
40-
$options = ['callback' => $options];
42+
if (!\is_array($callback) || (!isset($callback['callback']) && !isset($callback['groups']) && !isset($callback['payload']))) {
43+
$options['callback'] = $callback;
44+
} else {
45+
$options = array_merge($callback, $options);
4146
}
4247

43-
parent::__construct($options);
48+
parent::__construct($options, $groups, $payload);
4449
}
4550

4651
/**

src/Symfony/Component/Validator/Constraints/Choice.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <[email protected]>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2223
class Choice extends Constraint
2324
{
2425
const NO_SUCH_CHOICE_ERROR = '8e179f1b-97aa-4560-a02f-2a8b42e49df7';
@@ -49,4 +50,38 @@ public function getDefaultOption()
4950
{
5051
return 'choices';
5152
}
53+
54+
public function __construct(
55+
$choices = null,
56+
$callback = null,
57+
bool $multiple = null,
58+
bool $strict = null,
59+
int $min = null,
60+
int $max = null,
61+
string $message = null,
62+
string $multipleMessage = null,
63+
string $minMessage = null,
64+
string $maxMessage = null,
65+
$groups = null,
66+
$payload = null,
67+
array $options = []
68+
) {
69+
if (\is_array($choices) && \is_string(key($choices))) {
70+
$options = array_merge($choices, $options);
71+
} elseif (null !== $choices) {
72+
$options['choices'] = $choices;
73+
}
74+
75+
parent::__construct($options, $groups, $payload);
76+
77+
$this->callback = $callback ?? $this->callback;
78+
$this->multiple = $multiple ?? $this->multiple;
79+
$this->strict = $strict ?? $this->strict;
80+
$this->min = $min ?? $this->min;
81+
$this->max = $max ?? $this->max;
82+
$this->message = $message ?? $this->message;
83+
$this->multipleMessage = $multipleMessage ?? $this->multipleMessage;
84+
$this->minMessage = $minMessage ?? $this->minMessage;
85+
$this->maxMessage = $maxMessage ?? $this->maxMessage;
86+
}
5287
}

src/Symfony/Component/Validator/Constraints/GroupSequence.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
*
5252
* @author Bernhard Schussek <[email protected]>
5353
*/
54+
#[\Attribute(\Attribute::TARGET_CLASS)]
5455
class GroupSequence
5556
{
5657
/**

src/Symfony/Component/Validator/Constraints/GroupSequenceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <[email protected]>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_CLASS)]
2223
class GroupSequenceProvider
2324
{
2425
}

src/Symfony/Component/Validator/Constraints/IsFalse.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <[email protected]>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2223
class IsFalse extends Constraint
2324
{
2425
const NOT_FALSE_ERROR = 'd53a91b0-def3-426a-83d7-269da7ab4200';
@@ -28,4 +29,11 @@ class IsFalse extends Constraint
2829
];
2930

3031
public $message = 'This value should be false.';
32+
33+
public function __construct(array $options = null, string $message = null, array $groups = null, $payload = null)
34+
{
35+
parent::__construct($options ?? [], $groups, $payload);
36+
37+
$this->message = $message ?? $this->message;
38+
}
3139
}

src/Symfony/Component/Validator/Constraints/IsNull.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <[email protected]>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2223
class IsNull extends Constraint
2324
{
2425
const NOT_NULL_ERROR = '60d2f30b-8cfa-4372-b155-9656634de120';
@@ -28,4 +29,11 @@ class IsNull extends Constraint
2829
];
2930

3031
public $message = 'This value should be null.';
32+
33+
public function __construct(array $options = null, string $message = null, array $groups = null, $payload = null)
34+
{
35+
parent::__construct($options ?? [], $groups, $payload);
36+
37+
$this->message = $message ?? $this->message;
38+
}
3139
}

src/Symfony/Component/Validator/Constraints/IsTrue.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <[email protected]>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2223
class IsTrue extends Constraint
2324
{
2425
const NOT_TRUE_ERROR = '2beabf1c-54c0-4882-a928-05249b26e23b';
@@ -28,4 +29,11 @@ class IsTrue extends Constraint
2829
];
2930

3031
public $message = 'This value should be true.';
32+
33+
public function __construct(array $options = null, string $message = null, array $groups = null, $payload = null)
34+
{
35+
parent::__construct($options ?? [], $groups, $payload);
36+
37+
$this->message = $message ?? $this->message;
38+
}
3139
}

src/Symfony/Component/Validator/Constraints/NotBlank.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* @author Bernhard Schussek <[email protected]>
2222
* @author Kévin Dunglas <[email protected]>
2323
*/
24+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2425
class NotBlank extends Constraint
2526
{
2627
const IS_BLANK_ERROR = 'c1051bb4-d103-4f74-8988-acbcafc7fdc3';
@@ -33,9 +34,13 @@ class NotBlank extends Constraint
3334
public $allowNull = false;
3435
public $normalizer;
3536

36-
public function __construct($options = null)
37+
public function __construct(array $options = null, string $message = null, bool $allowNull = null, callable $normalizer = null, array $groups = null, $payload = null)
3738
{
38-
parent::__construct($options);
39+
parent::__construct($options ?? [], $groups, $payload);
40+
41+
$this->message = $message ?? $this->message;
42+
$this->allowNull = $allowNull ?? $this->allowNull;
43+
$this->normalizer = $normalizer ?? $this->normalizer;
3944

4045
if (null !== $this->normalizer && !\is_callable($this->normalizer)) {
4146
throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer)));

src/Symfony/Component/Validator/Constraints/NotNull.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <[email protected]>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2223
class NotNull extends Constraint
2324
{
2425
const IS_NULL_ERROR = 'ad32d13f-c3d4-423b-909a-857b961eb720';
@@ -28,4 +29,11 @@ class NotNull extends Constraint
2829
];
2930

3031
public $message = 'This value should not be null.';
32+
33+
public function __construct(array $options = null, string $message = null, array $groups = null, $payload = null)
34+
{
35+
parent::__construct($options ?? [], $groups, $payload);
36+
37+
$this->message = $message ?? $this->message;
38+
}
3139
}

0 commit comments

Comments
 (0)