Skip to content

[Validator] Added 'Any' validator #11586

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
wants to merge 4 commits into from
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
69 changes: 69 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Any.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?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;

/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION", "CLASS"})
*
* @author Cas Leentfaar <info@casleentfaar.com>
*/
class Any extends Composite
{
/**
* @var string
*/
public $message = 'None of the contraints found the value to be valid';

/**
* @var Constraint[]
*/
public $constraints = array();

/**
* {@inheritdoc}
*/
public function __construct($options = null)
{
parent::__construct($options);

$this->message = array_key_exists('message', $options) ? $options['message'] : $this->message;
}

/**
* {@inheritdoc}
*/
public function getDefaultOption()
{
return 'constraints';
}

/**
* {@inheritdoc}
*/
public function getRequiredOptions()
{
return array(
'constraints',
);
}

/**
* {@inheritdoc}
*/
protected function getCompositeOption()
{
return 'constraints';
}
}
78 changes: 78 additions & 0 deletions src/Symfony/Component/Validator/Constraints/AnyValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?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\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
* @author Cas Leentfaar <info@casleentfaar.com>
*/
class AnyValidator extends ConstraintValidator
{
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof Any) {
Copy link
Member

Choose a reason for hiding this comment

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

We never put such checks in Symfony

Copy link
Author

Choose a reason for hiding this comment

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

Could you clarify this? I just checked some validators and they all do this in their validate()-method, am I missing something here?

E.g., the CountryValidator:

    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof Country) {
            throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Country');
        }

Copy link
Member

Choose a reason for hiding this comment

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

right, I forgot we added it in March

throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\Any');
}

if (null === $value) {
return;
}

$context = $this->context;
$group = $context->getGroup();

if (!$context instanceof ExecutionContext) {
throw new \LogicException('Don\'t know how to deal with this when we need to create separate contexts later');
}

foreach ($constraint->constraints as $subConstraint) {
$subContext = new ExecutionContext(
$context->getValidator(),
$context->getRoot(),
$context->getTranslator(),
$context->getTranslationDomain()
);
if ($context instanceof ExecutionContextInterface) {
$subContext->getValidator()->validate($value, $subConstraint);
} else {
// 2.4 API
$subContext->validateValue($value, $subConstraint);
}
$violations = $subContext->getViolations();
if ($violations && $violations->count() === 0) {
Copy link
Member

Choose a reason for hiding this comment

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

given that you use the same context for all constraints, this will not work if the first constraint fails and the second one does not fail (as there will be a violation for the first one).

Thus, it also fails if another constraint already recorded a violation on the field outside the Any constraint.

The only way to do this cleanly is to use a separate context to validate each constraint (and you can stop iterating over constraint as soon as one of the is reported as valid).

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for all the feedback (so quick too!) I will get on this tonight.

As a side note, do you happen to know of a way to create a separate context without having to inject the required translator and validator services through my own validator/constraint?

return;
}
}

if ($this->context instanceof ExecutionContextInterface) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->setInvalidValue($value)
->addViolation();
} else {
// 2.4 API
$this->context->addViolation(
$constraint->message,
array('{{ value }}' => $value),
$value
);
}
}
}
16 changes: 16 additions & 0 deletions src/Symfony/Component/Validator/Context/ExecutionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,22 @@ public function getValidator()
return $this->validator;
}

/**
* {@inheritdoc}
*/
public function getTranslator()
{
return $this->translator;
}

/**
* {@inheritdoc}
*/
public function getTranslationDomain()
{
return $this->translationDomain;
}

/**
* {@inheritdoc}
*/
Expand Down
42 changes: 42 additions & 0 deletions src/Symfony/Component/Validator/Tests/Constraints/AnyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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 Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\Any;
use Symfony\Component\Validator\Constraints\Valid;

/**
* @author Cas Leentfaar <info@casleentfaar.com>
*/
class AnyTest extends \PHPUnit_Framework_TestCase
Copy link
Member

Choose a reason for hiding this comment

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

you should use the AbstractConstraintValidatorTest to provide a test against each API version

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, will get it fixed

Copy link
Member

Choose a reason for hiding this comment

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

Actually, my comment is for AnyValidatorTest

{
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testRejectNonConstraints()
{
new All(array(
'foo',
));
}

/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testRejectValidConstraint()
{
new Any(array(
new Valid(),
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?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 Symfony\Component\Validator\Constraints\Any;
use Symfony\Component\Validator\Constraints\AnyValidator;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Validation;

class AnyValidatorTest extends AbstractConstraintValidatorTest
{
protected function getApiVersion()
{
return Validation::API_VERSION_2_5;
}

protected function createValidator()
{
return new AnyValidator();
}

public function testNullIsValid()
{
$this->validator->validate(null, new Any(new Range(array('min' => 4))));

$this->assertNoViolation();
}

public function testWalkSingleConstraint()
{
$value = 5;
$constraint = new Range(array('min' => 4));

$this->validator->validate($value, new Any($constraint));

$this->assertNoViolation();
}

public function testWalkMultipleConstraints()
{
$value = 1;
$constraint1 = new Range(array('min' => 4));
$constraint2 = new NotNull();
$constraints = array($constraint1, $constraint2);

$this->validator->validate($value, new Any($constraints));

$this->assertNoViolation();
}

public function testNoConstraintValidated()
{
$value = 1;
$constraint1 = new Range(array('min' => 4));
$constraint2 = new NotNull();
$constraints = array($constraint1, $constraint2);
$any = new Any($constraints);

$this->validator->validate($value, $any);

$this->assertViolation($any->message);
}
}