From 13b797b666b0fb2ebf018062961c38cfef3086f8 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Wed, 14 Jul 2021 16:58:59 +0200 Subject: [PATCH] [Validator] Composite constraints as attributes Signed-off-by: Alexander M. Turek --- .../Component/Validator/Constraints/All.php | 23 +++ .../Validator/Constraints/Collection.php | 26 +++- .../Validator/Constraints/Composite.php | 4 +- .../Fixtures/CompositeAttribute/Entity.php | 131 ++++++++++++++++++ .../CompositeAttribute/EntityParent.php | 36 +++++ .../GroupSequenceProviderEntity.php | 34 +++++ .../Mapping/Loader/AnnotationLoaderTest.php | 3 + 7 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/Entity.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/EntityParent.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/GroupSequenceProviderEntity.php diff --git a/src/Symfony/Component/Validator/Constraints/All.php b/src/Symfony/Component/Validator/Constraints/All.php index d3fe49525f6fa..fa24d80547f79 100644 --- a/src/Symfony/Component/Validator/Constraints/All.php +++ b/src/Symfony/Component/Validator/Constraints/All.php @@ -11,16 +11,39 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Constraint; + /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class All extends Composite { public $constraints = []; + /** + * {@inheritdoc} + * + * @param mixed[]|Constraint[]|Constraint $constraints The nested constraints or an array of options + */ + public function __construct($constraints = null, array $groups = null) + { + if ($constraints instanceof Constraint) { + $options = ['constraints' => $constraints]; + } elseif (!\is_array($constraints)) { + throw new \TypeError(sprintf('%s: Parameter #1 is extected to be an array or an instance of %s, %s given.', __METHOD__, Constraint::class, get_debug_type($constraints))); + } elseif (\array_key_exists('constraints', $constraints) || \array_key_exists('value', $constraints)) { + $options = $constraints; + } else { + $options = ['constraints' => $constraints]; + } + + parent::__construct($options, $groups); + } + public function getDefaultOption() { return 'constraints'; diff --git a/src/Symfony/Component/Validator/Constraints/Collection.php b/src/Symfony/Component/Validator/Constraints/Collection.php index 6007b13318a56..749a599a71271 100644 --- a/src/Symfony/Component/Validator/Constraints/Collection.php +++ b/src/Symfony/Component/Validator/Constraints/Collection.php @@ -19,6 +19,7 @@ * * @author Bernhard Schussek */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Collection extends Composite { public const MISSING_FIELD_ERROR = '2fa2158c-2a7f-484b-98aa-975522539ff8'; @@ -38,15 +39,28 @@ class Collection extends Composite /** * {@inheritdoc} */ - public function __construct($options = null) - { + public function __construct( + array $fields = null, + bool $allowExtraFields = null, + bool $allowMissingFields = null, + string $extraFieldsMessage = null, + string $missingFieldsMessage = null, + array $groups = null + ) { // no known options set? $options is the fields array - if (\is_array($options) - && !array_intersect(array_keys($options), ['groups', 'fields', 'allowExtraFields', 'allowMissingFields', 'extraFieldsMessage', 'missingFieldsMessage'])) { - $options = ['fields' => $options]; + if (\is_array($fields) + && !array_intersect(array_keys($fields), ['groups', 'fields', 'allowExtraFields', 'allowMissingFields', 'extraFieldsMessage', 'missingFieldsMessage'])) { + $options = ['fields' => $fields]; + } else { + $options = $fields; } - parent::__construct($options); + parent::__construct($options, $groups); + + $this->allowExtraFields = $allowExtraFields ?? $this->allowExtraFields; + $this->allowMissingFields = $allowMissingFields ?? $this->allowMissingFields; + $this->extraFieldsMessage = $extraFieldsMessage ?? $this->extraFieldsMessage; + $this->missingFieldsMessage = $missingFieldsMessage ?? $this->missingFieldsMessage; } /** diff --git a/src/Symfony/Component/Validator/Constraints/Composite.php b/src/Symfony/Component/Validator/Constraints/Composite.php index b24da39d22855..61f55c7535192 100644 --- a/src/Symfony/Component/Validator/Constraints/Composite.php +++ b/src/Symfony/Component/Validator/Constraints/Composite.php @@ -51,9 +51,9 @@ abstract class Composite extends Constraint * cached. When constraints are loaded from the cache, no more group * checks need to be done. */ - public function __construct($options = null) + public function __construct($options = null, array $groups = null) { - parent::__construct($options); + parent::__construct($options, $groups); $this->initializeNestedConstraints(); diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/Entity.php b/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/Entity.php new file mode 100644 index 0000000000000..3de908c6ac5d1 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/Entity.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures\CompositeAttribute; + +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Tests\Fixtures\EntityInterfaceB; +use Symfony\Component\Validator\Tests\Fixtures\CallbackClass; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; + +#[ + ConstraintA, + Assert\GroupSequence(['Foo', 'Entity']), + Assert\Callback([CallbackClass::class, 'callback']), +] +class Entity extends EntityParent implements EntityInterfaceB +{ + #[ + Assert\NotNull, + Assert\Range(min: 3), + Assert\All([new Assert\NotNull(), new Assert\Range(min: 3)]), + Assert\All(constraints: [new Assert\NotNull(), new Assert\Range(min: 3)]), + Assert\Collection(fields: [ + 'foo' => [new Assert\NotNull(), new Assert\Range(min: 3)], + 'bar' => new Assert\Range(min: 5), + ]), + Assert\Choice(choices: ['A', 'B'], message: 'Must be one of %choices%'), + ] + public $firstName; + #[Assert\Valid] + public $childA; + #[Assert\Valid] + public $childB; + protected $lastName; + public $reference; + public $reference2; + private $internal; + public $data = 'Overridden data'; + public $initialized = false; + + public function __construct($internal = null) + { + $this->internal = $internal; + } + + public function getFirstName() + { + return $this->firstName; + } + + public function getInternal() + { + return $this->internal.' from getter'; + } + + public function setLastName($lastName) + { + $this->lastName = $lastName; + } + + #[Assert\NotNull] + public function getLastName() + { + return $this->lastName; + } + + public function getValid() + { + } + + #[Assert\IsTrue] + public function isValid() + { + return 'valid'; + } + + #[Assert\IsTrue] + public function hasPermissions() + { + return 'permissions'; + } + + public function getData() + { + return 'Overridden data'; + } + + #[Assert\Callback(payload: 'foo')] + public function validateMe(ExecutionContextInterface $context) + { + } + + #[Assert\Callback] + public static function validateMeStatic($object, ExecutionContextInterface $context) + { + } + + public function getChildA(): mixed + { + return $this->childA; + } + + public function setChildA(mixed $childA) + { + $this->childA = $childA; + } + + public function getChildB(): mixed + { + return $this->childB; + } + + public function setChildB(mixed $childB) + { + $this->childB = $childB; + } + + public function getReference() + { + return $this->reference; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/EntityParent.php b/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/EntityParent.php new file mode 100644 index 0000000000000..7bd7f6b954fe8 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/EntityParent.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures\CompositeAttribute; + +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Tests\Fixtures\EntityInterfaceA; + +class EntityParent implements EntityInterfaceA +{ + protected $firstName; + private $internal; + private $data = 'Data'; + private $child; + + #[NotNull] + protected $other; + + public function getData() + { + return 'Data'; + } + + public function getChild() + { + return $this->child; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/GroupSequenceProviderEntity.php b/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/GroupSequenceProviderEntity.php new file mode 100644 index 0000000000000..8159a4a638278 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CompositeAttribute/GroupSequenceProviderEntity.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures\CompositeAttribute; + +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\GroupSequenceProviderInterface; + +#[Assert\GroupSequenceProvider] +class GroupSequenceProviderEntity implements GroupSequenceProviderInterface +{ + public $firstName; + public $lastName; + + protected $sequence = []; + + public function __construct($sequence) + { + $this->sequence = $sequence; + } + + public function getGroupSequence() + { + return $this->sequence; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php index e59bfd0e6e2d5..47aa89f5748a9 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -185,5 +185,8 @@ public function provideNamespaces(): iterable if (\PHP_VERSION_ID >= 80000) { yield 'attributes' => ['Symfony\Component\Validator\Tests\Fixtures\Attribute']; } + if (\PHP_VERSION_ID >= 80100) { + yield 'composite attributes' => ['Symfony\Component\Validator\Tests\Fixtures\CompositeAttribute']; + } } }