Change how "Sf" rule works

Instead of creating the Symfony constraints itself "Sf" accepts an
instance of "Symfony\Component\Validator\Constraint".

Creating objects inside a rule, specially from an external library,
makes the rule too complex and also limits the possibilities with the
"Sf" rule since Symfony allows users to create complex validations (even
thought their API is not as simple as ours).

This commit also simplifies the way the messages are passed from Symfony
to the "Sf" when only one constraint has failed; instead of passing
the message of the whole constraint violation list, only the fist
constraint violation message it passed.

The problem that this rule will always have is that when using "Not" to
invert the validation we have a way to get a proper message since
Symfony Validator only return the result of constraints that failed.
That's something the Respect\Validation does in a similar way and to
change it a lot has to be changed.

These changes were checked in "symfony/validator" 4.0 and the version
was added to the "composer.json" file.

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2018-08-19 20:09:32 +02:00
parent fe7fed3461
commit 1da164a26e
No known key found for this signature in database
GPG key ID: 221E9281655813A6
7 changed files with 179 additions and 103 deletions

View file

@ -22,7 +22,7 @@
"malukenho/docheader": "^0.1.4",
"mikey179/vfsStream": "^1.6",
"phpunit/phpunit": "^6.4",
"symfony/validator": "^3.0",
"symfony/validator": "^3.0||^4.0",
"zendframework/zend-validator": "^2.0"
},
"suggest": {

View file

@ -1,19 +1,24 @@
# Sf
- `Sf(string $validatorName)`
- `Sf(Constraint $constraint)`
- `Sf(Constraint $constraint, ValidatorInterface $validator)`
Use Symfony2 validators inside Respect\Validation flow. Messages
are preserved.
Validate the input with a Symfony Validator (>=4.0 or >=3.0) Constraint.
```php
v::sf('Time')->validate('15:00:00');
use Symfony\Component\Validator\Constraint\Iban;
v::sf(new Iban())->validate('NL39 RABO 0300 0652 64'); // true
```
This rule will keep all the messages returned from Symfony.
## Changelog
Version | Description
--------|-------------
2.0.0 | Upgraded support to version >=3.0.0 of Symfony Validator
2.0.0 | Do not create constraints anymore
2.0.0 | Upgraded support to version >=4.0 or >=3.0 of Symfony Validator
0.3.9 | Created
***

View file

@ -13,14 +13,21 @@ declare(strict_types=1);
namespace Respect\Validation\Exceptions;
class SfException extends ValidationException
/**
* @author Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
* @author Henrique Moody <henriquemoody@gmail.com>
*/
final class SfException extends ValidationException
{
/**
* {@inheritdoc}
*/
public static $defaultTemplates = [
self::MODE_DEFAULT => [
self::STANDARD => '{{name}}',
self::STANDARD => '{{name}} must be valid for {{constraint}}',
],
self::MODE_NEGATIVE => [
self::STANDARD => '{{name}}',
self::STANDARD => '{{name}} must not be valid for {{constraint}}',
],
];
}

View file

@ -13,62 +13,83 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use ReflectionClass;
use ReflectionException;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\SfException;
use Respect\Validation\Exceptions\ValidationException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class Sf extends AbstractRule
/**
* Validate the input with a Symfony Validator (>=4.0 or >=3.0) Constraint.
*
* @author Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
* @author Augusto Pascutti <augusto@phpsp.org.br>
* @author Henrique Moody <henriquemoody@gmail.com>
*/
final class Sf extends AbstractRule
{
public const SYMFONY_CONSTRAINT_NAMESPACE = 'Symfony\Component\Validator\Constraints\%s';
public $name;
/**
* @var Constraint
*/
private $constraint;
public function __construct($name, array $params = [])
/**
* @var ValidatorInterface
*/
private $validator;
/**
* Initializes the rule with the Constraint and the Validator.
*
* In the the Validator is not defined, tries to create one.
*
* @param Constraint $constraint
* @param ValidatorInterface|null $validator
*/
public function __construct(Constraint $constraint, ValidatorInterface $validator = null)
{
$this->name = ucfirst($name);
$this->constraint = $this->createSymfonyConstraint($this->name, $params);
}
private function createSymfonyConstraint($constraintName, array $constraintConstructorParameters = [])
{
$fullClassName = sprintf(self::SYMFONY_CONSTRAINT_NAMESPACE, $constraintName);
try {
$constraintReflection = new ReflectionClass($fullClassName);
} catch (ReflectionException $previousException) {
$baseExceptionMessage = 'Symfony/Validator constraint "%s" does not exist.';
$exceptionMessage = sprintf($baseExceptionMessage, $constraintName);
throw new ComponentException($exceptionMessage, 0, $previousException);
}
if ($constraintReflection->hasMethod('__construct')) {
return $constraintReflection->newInstanceArgs($constraintConstructorParameters);
}
return $constraintReflection->newInstance();
}
private function returnViolationsForConstraint($valueToValidate, Constraint $symfonyConstraint)
{
$validator = Validation::createValidator(); // You gotta love those Symfony namings
return $validator->validate($valueToValidate, $symfonyConstraint);
$this->constraint = $constraint;
$this->validator = $validator ?: Validation::createValidator();
}
/**
* {@inheritdoc}
*/
public function assert($input): void
{
$violations = $this->returnViolationsForConstraint($input, $this->constraint);
if (0 == count($violations)) {
$violations = $this->validator->validate($input, $this->constraint);
if (0 === $violations->count()) {
return;
}
throw $this->reportError((string) $violations);
if (1 === $violations->count()) {
throw $this->reportError($input, ['violations' => $violations[0]->getMessage()]);
}
throw $this->reportError($input, ['violations' => trim((string) $violations)]);
}
/**
* {@inheritdoc}
*/
public function reportError($input, array $extraParams = []): ValidationException
{
$exception = parent::reportError($input, $extraParams);
if (isset($extraParams['violations'])) {
$exception->updateTemplate($extraParams['violations']);
}
return $exception;
}
/**
* {@inheritdoc}
*/
public function validate($input): bool
{
$violations = $this->returnViolationsForConstraint($input, $this->constraint);
if (count($violations)) {
try {
$this->assert($input);
} catch (SfException $exception) {
return false;
}

View file

@ -18,6 +18,8 @@ use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Rules\AllOf;
use Respect\Validation\Rules\Key;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @method static Validator allOf(Validatable ...$rule)
@ -136,7 +138,7 @@ use Respect\Validation\Rules\Key;
* @method static Validator resourceType()
* @method static Validator roman()
* @method static Validator scalarVal()
* @method static Validator sf(string $name, array $params = null)
* @method static Validator sf(Constraint $constraint, ValidatorInterface $validator = null)
* @method static Validator size(string $minSize = null, string $maxSize = null)
* @method static Validator slug()
* @method static Validator space(string $additionalChars = null)

View file

@ -0,0 +1,55 @@
--FILE--
<?php
require 'vendor/autoload.php';
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Exceptions\SfException;
use Respect\Validation\Validator as v;
use Symfony\Component\Validator\Constraints\Collection;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\IsNull;
try {
v::sf(new IsNull())->check('something');
} catch (SfException $exception) {
echo $exception->getMessage().PHP_EOL;
}
try {
v::not(v::sf(new IsNull()))->check(null);
} catch (SfException $exception) {
echo $exception->getMessage().PHP_EOL;
}
try {
v::sf(new Email())->assert('not-null');
} catch (NestedValidationException $exception) {
echo $exception->getFullMessage().PHP_EOL;
}
try {
v::not(v::sf(new Email()))->assert('example@example.com');
} catch (NestedValidationException $exception) {
echo $exception->getFullMessage().PHP_EOL;
}
try {
v::sf(
new Collection([
'first' => new IsNull(),
'second' => new Email(),
])
)->check(['second' => 'not-email']);
} catch (SfException $exception) {
echo $exception->getMessage();
}
?>
--EXPECTF--
This value should be null.
`NULL` must not be valid for `[object] (Symfony\Component\Validator\Constraints\IsNull: { %s })`
- This value is not a valid email address.
- "example@example.com" must not be valid for `[object] (Symfony\Component\Validator\Constraints\Email: { %s })`
Array[first]:
This field is missing. (code %s)
Array[second]:
This value is not a valid email address. (code %s)

View file

@ -14,84 +14,70 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\TestCase;
use Respect\Validation\Exceptions\AllOfException;
use Respect\Validation\Validator as v;
use stdClass;
use Symfony\Component\Validator\Constraints\IsFalse;
use Symfony\Component\Validator\Constraints\IsNull;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\TraceableValidator;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @group rule
* @covers \Respect\Validation\Exceptions\SfException
* @group rule
*
* @covers \Respect\Validation\Rules\Sf
*
* @author Augusto Pascutti <augusto@phpsp.org.br>
* @author Gabriel Caruso <carusogabriel34@gmail.com>
* @author Henrique Moody <henriquemoody@gmail.com>
*/
class SfTest extends TestCase
final class SfTest extends TestCase
{
/**
* @test
*/
public function validationWithAnExistingValidationConstraint(): void
public function itShouldValidateWithDefinedConstraintAndValidator(): void
{
$constraintName = 'Time';
$validConstraintValue = '04:20:00';
$invalidConstraintValue = 'yada';
self::assertTrue(
v::sf($constraintName)->validate($validConstraintValue),
sprintf('"%s" should be valid under "%s" constraint.', $validConstraintValue, $constraintName)
);
self::assertFalse(
v::sf($constraintName)->validate($invalidConstraintValue),
sprintf('"%s" should be invalid under "%s" constraint.', $invalidConstraintValue, $constraintName)
);
$sut = new Sf(new IsNull());
self::assertTrue($sut->validate(null));
}
/**
* @doesNotPerformAssertions
*
* @depends validationWithAnExistingValidationConstraint
*
* @test
*/
public function assertionWithAnExistingValidationConstraint(): void
public function itShouldInvalidateWithDefinedConstraintAndValidator(): void
{
$constraintName = 'Time';
$validConstraintValue = '04:20:00';
v::sf($constraintName)->assert($validConstraintValue);
$sut = new Sf(new IsFalse());
self::assertFalse($sut->validate(true));
}
/**
* @depends assertionWithAnExistingValidationConstraint
*
* @test
*/
public function assertionMessageWithAnExistingValidationConstraint()
public function itShouldHaveAValidatorByDefault(): void
{
$constraintName = 'Time';
$invalidConstraintValue = '34:90:70';
try {
v::sf($constraintName)->assert($invalidConstraintValue);
} catch (AllOfException $exception) {
$fullValidationMessage = $exception->getFullMessage();
$expectedValidationException = <<<'EOF'
- Time
EOF;
$sut = new Sf(new IsNull());
return self::assertEquals(
$expectedValidationException,
$fullValidationMessage,
'Exception message is different from the one expected.'
);
self::assertAttributeInstanceOf(ValidatorInterface::class, 'validator', $sut);
}
/**
* @test
*/
public function itShouldUseTheDefinedValidatorToValidate(): void
{
if (!class_exists(TraceableValidator::class)) {
self::markTestSkipped('The current version of Symfony Validator does not have '.TraceableValidator::class);
}
self::fail('Validation exception expected to compare message.');
}
/**
* @expectedException \Respect\Validation\Exceptions\ComponentException
* @expectedExceptionMessage Symfony/Validator constraint "FluxCapacitor" does not exist.
*
* @test
*/
public function validationWithNonExistingConstraint(): void
{
$fantasyConstraintName = 'FluxCapacitor';
$fantasyValue = '8GW';
v::sf($fantasyConstraintName)->validate($fantasyValue);
$input = new stdClass();
$validator = new TraceableValidator(Validation::createValidator());
$sut = new Sf(new IsNull(), $validator);
$sut->validate($input);
self::assertSame($input, $validator->getCollectedData()[0]['context']['value']);
}
}