Skip to content

[Validator] Add option to override property constraints in child class #36155

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* Disables overriding property constraints of parent class.
*
* Using the annotations on a property has higher precedence than using it on a class.
*
* @Annotation
*
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
*/
class DisableOverridingPropertyConstraints extends Constraint
{
public function __construct($options = null)
{
if (\is_array($options) && \array_key_exists('groups', $options)) {
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
}

parent::__construct($options);
}

/**
* {@inheritdoc}
*/
public function getTargets()
{
return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* Enables overriding property constraints of parent class.
*
* Using the annotations on a property has higher precedence than using it on a class.
*
* @Annotation
*
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
*/
class EnableOverridingPropertyConstraints extends Constraint
{
public function __construct($options = null)
{
if (\is_array($options) && \array_key_exists('groups', $options)) {
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
}

parent::__construct($options);
}

/**
* {@inheritdoc}
*/
public function getTargets()
{
return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
}
}
29 changes: 29 additions & 0 deletions src/Symfony/Component/Validator/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,12 +311,41 @@ public function mergeConstraints(self $source)
$this->setGroupSequenceProvider(true);
}

if (OverridingPropertyConstraintsStrategy::NONE === $this->getOverridingPropertyConstraintsStrategy()) {
$this->overridingPropertyConstraintsStrategy = $source->getOverridingPropertyConstraintsStrategy();
}

foreach ($source->getConstraints() as $constraint) {
$this->addConstraint(clone $constraint);
}

foreach ($source->getConstrainedProperties() as $property) {
$strategy = null;

foreach ($this->getPropertyMetadata($property) as $childMember) {
// overriding is enabled in property
if (OverridingPropertyConstraintsStrategy::ENABLED === $strategy = $childMember->getOverridingPropertyConstraintsStrategy()) {
continue 2;
}
}

foreach ($source->getPropertyMetadata($property) as $member) {
// property is overridden, but property constraints strategy is not set explicitly in property
if (OverridingPropertyConstraintsStrategy::NONE === $strategy) {
// class' strategy takes precedence over parent class property's strategy, if both are set
if (OverridingPropertyConstraintsStrategy::NONE === $strategy = $this->getOverridingPropertyConstraintsStrategy()) {
$strategy = $member->getOverridingPropertyConstraintsStrategy();
}

foreach ($this->getPropertyMetadata($property) as $childMember) {
$childMember->overridingPropertyConstraintsStrategy = $strategy;
}

if (OverridingPropertyConstraintsStrategy::ENABLED === $strategy) {
continue 2;
}
}

$member = clone $member;

foreach ($member->getConstraints() as $constraint) {
Expand Down
33 changes: 33 additions & 0 deletions src/Symfony/Component/Validator/Mapping/GenericMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
use Symfony\Component\Validator\Constraints\DisableOverridingPropertyConstraints;
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
use Symfony\Component\Validator\Constraints\EnableOverridingPropertyConstraints;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
Expand Down Expand Up @@ -88,6 +90,21 @@ class GenericMetadata implements MetadataInterface
*/
public $autoMappingStrategy = AutoMappingStrategy::NONE;

/**
* The strategy for merging/overriding property constraints, when extending class that has property constraints.
*
* By default, property constraints in a child class are merged with property constraints of a parent class.
*
* @var int
*
* @see OverridingPropertyConstraintsStrategy
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getOverridingPropertyConstraintsStrategy()} instead.
*/
public $overridingPropertyConstraintsStrategy = OverridingPropertyConstraintsStrategy::NONE;

/**
* Returns the names of the properties that should be serialized.
*
Expand All @@ -101,6 +118,7 @@ public function __sleep()
'cascadingStrategy',
'traversalStrategy',
'autoMappingStrategy',
'overridingPropertyConstraintsStrategy',
];
}

Expand Down Expand Up @@ -160,6 +178,13 @@ public function addConstraint(Constraint $constraint)
return $this;
}

if ($constraint instanceof EnableOverridingPropertyConstraints || $constraint instanceof DisableOverridingPropertyConstraints) {
$this->overridingPropertyConstraintsStrategy = $constraint instanceof EnableOverridingPropertyConstraints ? OverridingPropertyConstraintsStrategy::ENABLED : OverridingPropertyConstraintsStrategy::DISABLED;

// The constraint is not added
return $this;
}

$this->constraints[] = $constraint;

foreach ($constraint->groups as $group) {
Expand Down Expand Up @@ -236,4 +261,12 @@ public function getAutoMappingStrategy(): int
{
return $this->autoMappingStrategy;
}

/**
* @see OverridingPropertyConstraintsStrategy
*/
public function getOverridingPropertyConstraintsStrategy(): int
{
return $this->overridingPropertyConstraintsStrategy;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Mapping;

/**
* Specifies whether class' property constraints should be merged with
* or override property constraints of a parent class.
*
* Overriding property constraints works for properties that override parent property of same name.
*
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
*/
final class OverridingPropertyConstraintsStrategy
{
/**
* Nothing explicitly set.
* In case of property, rely on class strategy.
* In case of class, inherit strategy from parent class.
*/
public const NONE = 0;

/**
* Specifies that class' property constraints should be merged with constraints of a parent class.
*/
public const DISABLED = 1;

/**
* Specifies that class' property constraints should override constraints of a parent class.
*/
public const ENABLED = 2;

/**
* Not instantiable.
*/
private function __construct()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Tests\Constraints;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\DisableOverridingPropertyConstraints;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
*/
class DisableOverridingPropertyConstraintsTest extends TestCase
{
public function testGroups()
{
$this->expectException(ConstraintDefinitionException::class);
$this->expectExceptionMessage(sprintf('The option "groups" is not supported by the constraint "%s".', DisableOverridingPropertyConstraints::class));

new DisableOverridingPropertyConstraints(['groups' => 'foo']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Tests\Constraints;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\EnableOverridingPropertyConstraints;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
*/
class EnableOverridingPropertyConstraintsTest extends TestCase
{
public function testGroups()
{
$this->expectException(ConstraintDefinitionException::class);
$this->expectExceptionMessage(sprintf('The option "groups" is not supported by the constraint "%s".', EnableOverridingPropertyConstraints::class));

new EnableOverridingPropertyConstraints(['groups' => 'foo']);
}
}
Loading