8000 [Validator] Add "is_valid" function to the Expression constraint · symfony/symfony@324a374 · GitHub
[go: up one dir, main page]

Skip to content

Commit 324a374

Browse files
committed
[Validator] Add "is_valid" function to the Expression constraint
1 parent 2b71c6f commit 324a374

File tree

7 files changed

+218
-2
lines changed

7 files changed

+218
-2
lines changed

src/Symfony/Component/ExpressionLanguage/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
4.4.0
5+
-----
6+
7+
* Added the `ExpressionLanguage::hasFunction()` method.
8+
49
4.0.0
510
-----
611

src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ public function registerProvider(ExpressionFunctionProviderInterface $provider)
132132
}
133133
}
134134

135+
public function hasFunction(string $name): bool
136+
{
137+
return isset($this->functions[$name]);
138+
}
139+
135140
protected function registerFunctions()
136141
{
137142
$this->addFunction(ExpressionFunction::fromPhp('constant'));

src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,15 @@ function (ExpressionLanguage $el) {
256256
],
257257
];
258258
}
259+
260+
public function testHasFunction()
261+
{
262+
$el = new ExpressionLanguage();
263+
264+
$this->assertFalse($el->hasFunction('foo'));
265+
266+
$el->register('foo', function () {}, function () {});
267+
268+
$this->assertTrue($el->hasFunction('foo'));
269+
}
259270
}

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ CHANGELOG
2424
* Overriding the methods `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` without the `void` return-type is deprecated.
2525
* deprecated `Symfony\Component\Validator\Mapping\Cache\CacheInterface` in favor of PSR-6.
2626
* deprecated `ValidatorBuilder::setMetadataCache`, use `ValidatorBuilder::setMappingCache` instead.
27+
* Added the `is_valid` function to the `Expression` constraint.
2728

2829
4.3.0
2930
-----

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

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
1515
use Symfony\Component\Validator\Constraint;
1616
use Symfony\Component\Validator\ConstraintValidator;
17+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1718
use Symfony\Component\Validator\Exception\LogicException;
1819
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
1920

@@ -39,7 +40,9 @@ public function __construct(/*ExpressionLanguage */$expressionLanguage = null)
3940
@trigger_error(sprintf('The "%s" first argument must be an instance of "%s" or null since 4.4. "%s" given', __METHOD__, ExpressionLanguage::class, \is_object($expressionLanguage) ? \get_class($expressionLanguage) : \gettype($expressionLanguage)), E_USER_DEPRECATED);
4041
}
4142

42-
$this->expressionLanguage = $expressionLanguage;
43+
if (($this->expressionLanguage = $expressionLanguage) instanceof ExpressionLanguage) {
44+
$this->addIsValidFunction();
45+
}
4346
}
4447

4548
/**
@@ -65,13 +68,87 @@ public function validate($value, Constraint $constraint)
6568

6669
private function getExpressionLanguage()
6770
{
68-
if (null === $this->expressionLanguage) {
71+
if (!$this->expressionLanguage instanceof ExpressionLanguage) {
6972
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
7073
throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
7174
}
7275
$this->expressionLanguage = new ExpressionLanguage();
76+
77+
$this->addIsValidFunction();
7378
}
7479

7580
return $this->expressionLanguage;
7681
}
82+
83+
private function addIsValidFunction(): void
84+
{
85+
if ($this->expressionLanguage->hasFunction('is_valid')) {
86+
return;
87+
}
88+
89+
$this->expressionLanguage->register('is_valid', function () {
90+
throw new LogicException('The "is_valid" function cannot be compiled.');
91+
}, function (array $variables, ...$arguments): bool {
92+
if (!$arguments) {
93+
throw new ConstraintDefinitionException('The "is_valid" function requires at least one argument.');
94+
}
95+
96+
$isObject = \is_object($object = $this->context->getObject());
97+
98+
$constraints = [];
99+
$properties = [];
100+
101+
foreach ($arguments as $argument) {
102+
if ($argument instanceof Constraint) {
103+
$constraints[] = $argument;
104+
105+
continue;
106+
}
107+
108+
if (\is_array($argument)) {
109+
foreach ($argument as $constraint) {
110+
if (!$constraint instanceof Constraint) {
111+
throw new ConstraintDefinitionException(sprintf('The "is_valid" function only accepts arrays that contain instances of "%s" exclusively, "%s" given.', Constraint::class, \is_object($constraint) ? \get_class($constraint) : \gettype($constraint)));
112+
}
113+
114+
$constraints[] = $constraint;
115+
}
116+
117+
continue;
118+
}
119+
120+
if (\is_string($argument)) {
121+
if (!$isObject) {
122+
throw new ConstraintDefinitionException('The "is_valid" function only accepts strings that represent properties paths when validating an object.');
123+
}
124+
125+
$properties[] = $argument;
126+
127+
continue;
128+
}
129+
130+
throw new ConstraintDefinitionException(sprintf('The "is_valid" function only accepts instances of "%s", arrays of "%s", or strings that represent properties paths (when validating an object), "%s" given.', Constraint::class, Constraint::class, \is_object($argument) ? \get_class($argument) : \gettype($argument)));
131+
}
132+
133+
if (!$constraints && !$properties) {
134+
return true;
135+
}
136+
137+
$validator = $this->context->getValidator();
138+
139+
if ($constraints) {
140+
if ($validator->validate($variables['value'], $constraints, $this->context->getGroup())->count()) {
141+
return false;
142+
}
143+
}
144+
145+
foreach ($properties as $property) {
146+
if ($validator->validateProperty($object, $property, $this->context->getGroup())->count()) {
147+
return false;
148+
}
149+
}
150+
151+
return true;
152+
});
153+
}
77154
}

src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,20 @@
1414
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
1515
use Symfony\Component\Validator\Constraints\Expression;
1616
use Symfony\Component\Validator\Constraints\ExpressionValidator;
17+
use Symfony\Component\Validator\Constraints\IdenticalTo;
18+
use Symfony\Component\Validator\Constraints\IsNull;
19+
use Symfony\Component\Validator\Constraints\NotIdenticalTo;
20+
use Symfony\Component\Validator\Constraints\NotNull;
21+
use Symfony\Component\Validator\Constraints\Type;
22+
use Symfony\Component\Validator\Context\ExecutionContext;
23+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
24+
use Symfony\Component\Validator\Mapping\ClassMetadata;
1725
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
1826
use Symfony\Component\Validator\Tests\Fixtures\Entity;
27+
use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory;
1928
use Symfony\Component\Validator\Tests\Fixtures\ToString;
29+
use Symfony\Component\Validator\ValidatorBuilder;
30+
use Symfony\Contracts\Translation\TranslatorInterface;
2031

2132
class ExpressionValidatorTest extends ConstraintValidatorTestCase
2233
{
@@ -322,4 +333,108 @@ public function testPassingCustomValues()
322333

323334
$this->assertNoViolation();
324335
}
336+
337+
public function testExistingIsValidFunctionIsNotOverridden()
338+
{
339+
$used = false;
340+
341+
$el = $el = new ExpressionLanguage();
342+
$el->register('is_valid', function () {}, function () use (&$used) {
343+
$used = true;
344+
});
345+
346+
$validator = new ExpressionValidator($el);
347+
$validator->initialize($this->context);
348+
349+
$validator->validate('foo', new Expression('is_valid()'));
350+
351+
$this->assertTrue($used);
352+
}
353+
354+
/**
355+
* @dataProvider isValidFunctionWithInvalidArgumentsProvider
356+
*/
357+
public function testIsValidFunctionWithInvalidArguments(string $expectedMessage, $value, string $expression, array $values)
358+
{
359+
$this->expectException(ConstraintDefinitionException::class);
360+
$this->expectExceptionMessage($expectedMessage);
361+
362+
$this->validator->validate($value, new Expression([
363+
'expression' => $expression,
364+
'values' => $values,
365+
]));
366+
}
367+
368+
public function isValidFunctionWithInvalidArgumentsProvider()
369+
{
370+
return [
371+
['The "is_valid" function requires at least one argument.', null, 'is_valid()', []],
372+
['The "is_valid" function only accepts instances of "Symfony\Component\Validator\Constraint", arrays of "Symfony\Component\Validator\Constraint", or strings that represent properties paths (when validating an object), "ArrayIterator" given.', null, 'is_valid(a)', ['a' => new \ArrayIterator()]],
373+
['The "is_valid" function only accepts instances of "Symfony\Component\Validator\Constraint", arrays of "Symfony\Component\Validator\Constraint", or strings that represent properties paths (when validating an object), "NULL" given.', null, 'is_valid(a)', ['a' => null]],
374+
['The "is_valid" function only accepts arrays that contain instances of "Symfony\Component\Validator\Constraint" exclusively, "ArrayIterator" given.', null, 'is_valid(a)', ['a' => [new \ArrayIterator()]]],
375+
['The "is_valid" function only accepts arrays that contain instances of "Symfony\Component\Validator\Constraint" exclusively, "string" given.', null, 'is_valid(a)', ['a' => ['foo']]],
376+
['The "is_valid" function only accepts strings that represent properties paths when validating an object.', 'foo', 'is_valid("bar")', []],
377+
];
378+
}
379+
380+
/**
381+
* @dataProvider isValidFunctionProvider
382+
*/
383+
public function testIsValidFunction(bool $shouldBeValid, $value, string $expression, array $values = [], string $group = null, array $propertiesConstraints = [])
384+
{
385+
$translator = $this->createMock(TranslatorInterface::class);
386+
$translator->method('trans')->willReturnArgument(0);
387+
388+
$validatorBuilder = new ValidatorBuilder();
389+
390+
$classMetadata = null;
391+
if ($valueIsObject = \is_object($value)) {
392+
$classMetadata = new ClassMetadata(\get_class($value));
393+
foreach ($propertiesConstraints as $property => $constraints) {
394+
$classMetadata->addPropertyConstraints($property, $constraints);
395+
}
396+
397+
$validatorBuilder->setMetadataFactory((new FakeMetadataFactory())->addMetadata($classMetadata));
398+
}
399+
400+
$this->validator->initialize($executionContext = new ExecutionContext(
401+
$validatorBuilder->getValidator(),
402+
$this->root,
403+
$translator
404+
));
405+
406+
$executionContext->setConstraint($constraint = new Expression([
407+
'expression' => $expression,
408+
'values' => $values,
409+
]));
410+
$executionContext->setGroup($group);
411+
$executionContext->setNode($value, $valueIsObject ? $value : null, $classMetadata, '');
412+
413+
$this->validator->validate($value, $constraint);
414+
415+
$this->assertSame($shouldBeValid, !(bool) $executionContext->getViolations()->count());
416+
}
417+
418+
public function isValidFunctionProvider()
419+
{
420+
return [
421+
[true, 'foo', 'is_valid(a) or is_valid(b)', ['a' => new NotIdenticalTo('foo'), 'b' => new IdenticalTo('foo')]],
422+
[false, 'foo', 'is_valid(a) and is_valid(b)', ['a' => new NotIdenticalTo('foo'), 'b' => new IdenticalTo('foo')]],
423+
[false, 'foo', 'is_valid(a, b)', ['a' => new NotIdenticalTo('foo'), 'b' => new IdenticalTo('foo')]],
424+
[false, 'foo', 'is_valid(a)', ['a' => new NotIdenticalTo('foo')]],
425+
[true, 'foo', 'is_valid(a)', ['a' => [new IdenticalTo('foo')]]],
426+
[true, 'foo', 'is_valid(a)', ['a' => new NotIdenticalTo(['value' => 'foo', 'groups' => 'g1'])], 'g2'],
427+
[false, new TestExpressionValidatorObject(), 'is_valid("foo")', [], null, ['foo' => [new NotNull()]]],
428+
[true, new TestExpressionValidatorObject(), 'is_valid("foo")', [], null, ['foo' => [new IsNull()]]],
429+
[true, new TestExpressionValidatorObject(), 'is_valid("foo")'],
430+
[true, new TestExpressionValidatorObject(), 'is_valid("any string")'],
431+
[false, new TestExpressionValidatorObject(), 'is_valid("foo", a)', ['a' => new IsNull()], null, ['foo' => [new IsNull()]]],
432+
[true, new TestExpressionValidatorObject(), 'is_valid(a, "foo")', ['a' => new Type(TestExpressionValidatorObject::class)], null, ['foo' => [new IsNull()]]],
433+
];
434+
}
435+
}
436+
437+
final class TestExpressionValidatorObject
438+
{
439+
public $foo = null;
325440
}

src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public function hasMetadataFor($class): bool
6262
public function addMetadata($metadata)
6363
{
6464
$this->metadatas[$metadata->getClassName()] = $metadata;
65+
66+
return $this;
6567
}
6668

6769
public function addMetadataForValue($value, MetadataInterface $metadata)

0 commit comments

Comments
 (0)
0