Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Validator] Access container array in Expression/Context #23134

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 11 commits into from

Conversation

Engerim
Copy link
Contributor

@Engerim Engerim commented Jun 11, 2017

Q A
Branch? 3.4
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes/no
Fixed tickets #12315
License MIT
Doc PR symfony/symfony-docs#...

This solves an old issues with arrays and the expression validator. Below is a melody script which throws an exception because "this" is null and not an array like expected.

This PR adds a dataPath property to the Expression constraint, which allows the validator to retrieve the "this" value in an other way from the context.

I'm not sure about the name "dataPath", so i'm open for a better naming.

Documentation PR will be added later, if this PR is approved.

<?php
<<<CONFIG
packages:
    - "symfony/form: ~3.3.0"
    - "symfony/validator: ~3.3.0"
    - "symfony/expression-language: ~3.3.0"
CONFIG;

use Symfony\Component\Form\Forms;
use Symfony\Component\Validator\Constraints\Expression;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Validator\ValidatorBuilder;


$form = Forms::createFormFactoryBuilder()->addExtension(new ValidatorExtension((new ValidatorBuilder())->getValidator()))->getFormFactory()->createBuilder()
    ->add('dateStart', DateType::class, [
        'constraints' => [
            new Expression([
                'expression' => 'this["dateStart"] <= this["dateEnd"]'
            ]),
        ],
    ])
    ->add('dateEnd', DateType::class, [
        'constraints' => [
            new Expression([
                'expression' => 'this["dateEnd"] >= this["dateStart"]'
            ]),
        ],
    ])
    ->getForm();


$form->submit(['dateEnd' => '2011-06-05', 'dataStart' => '2011-06-07']);

@@ -46,6 +47,10 @@ public function validate($value, Constraint $constraint)
$variables['value'] = $value;
$variables['this'] = $this->context->getObject();

if (strlen($constraint->dataPath) > 0) {
Copy link
Member

Choose a reason for hiding this comment

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

This is correct but following the code base style I suggest null !== $constraint->dataPath to avoid function call?

Copy link
Member

Choose a reason for hiding this comment

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

should be null !== $constraint->dataPath && '' !== $constraint->dataPath

Copy link
Member

Choose a reason for hiding this comment

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

What about allowing the $dataPath option to also be a PropertyPath instance?

Copy link
Contributor Author

@Engerim Engerim Jun 19, 2017

Choose a reason for hiding this comment

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

Hm i don't like this because than it can be a string or instanceof PropertyPath. If we keep in mind that we want to use later on scalar type hints for version 4.0, we would have a problem.

@@ -46,6 +47,10 @@ public function validate($value, Constraint $constraint)
$variables['value'] = $value;
$variables['this'] = $this->context->getObject();

if (strlen($constraint->dataPath) > 0) {
$variables['this'] = $this->getPropertyAccessor()->getValue($this->context, $constraint->dataPath);
Copy link
Member

Choose a reason for hiding this comment

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

if $variables['this'] may be overridden I suggest move $variables['this'] = $this->context->getObject(); to else statement?

@@ -46,6 +47,10 @@ public function validate($value, Constraint $constraint)
$variables['value'] = $value;
$variables['this'] = $this->context->getObject();

if (strlen($constraint->dataPath) > 0) {
$variables['this'] = $this->getPropertyAccessor()->getValue($this->context, $constraint->dataPath);
Copy link
Member

Choose a reason for hiding this comment

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

In the other hand, if Symfony deciders decides to make it configurable I think we could start the path from the root i.e.:

$this->getPropertyAccessor()->getValue($this->context->getRoot(), $constraint->dataPath);

else, I suggest pass the root value to this directly if it's null i.e::

if (null === $variables['this'] = $this->context->getObject()) {
    $variables['this'] = $this->context->getRoot();
}

thus, you don't have to worry about the "dataPath", you know the data linked to the root, therefore you just have to access them:

new Expression(array('expression' => 'value <= this["data"]["dateEnd"]'));

wdyt?

Copy link
Contributor Author

@Engerim Engerim Jun 11, 2017

Choose a reason for hiding this comment

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

I like the idea, to pass the root value directly to this if it's null. No dataPath and so easier to use. I will wait to change it because I would like to know how the deciders think about this.

@yceruto
Copy link
Member

yceruto commented Jun 11, 2017

This one could fix this #22403 ticket too ;)

@nicolas-grekas nicolas-grekas changed the base branch from master to 3.4 June 14, 2017 08:07
@nicolas-grekas
Copy link
Member

@Engerim I changed the base branch to 3.4. Can you please rebase on branch 3.4 and push again on your side?

@nicolas-grekas nicolas-grekas added this to the 3.4 milestone Jun 14, 2017
@Engerim
Copy link
Contributor Author

Engerim commented Jun 19, 2017

@nicolas-grekas sorry for the late response, i rebased and pushed again.

@@ -235,4 +235,17 @@ public function testExpressionLanguageUsage()

$this->assertTrue($used, 'Failed asserting that custom ExpressionLanguage instance is used.');
}

public function testExpressionLanguageUsageWithCustomDataPath()
Copy link
Contributor

Choose a reason for hiding this comment

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

does this name make sense? Maybe just testExpressionWithCustomDataPath?

Copy link
Contributor Author

@Engerim Engerim Jun 20, 2017

Choose a reason for hiding this comment

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

I orientated me on the naming from the test function above with name testExpressionLanguageUsage . But I can change it for better understanding

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes I saw that. But the previous test actually tests that passing in a custom ExpressionLanguage instance works. Hence the name 😄

Alexander Miehe added 6 commits June 28, 2017 15:30
* add dataPath Property to extract "this" in a different way from the context
* update changelog
* change string check
* change test method name to a better name
private function getPropertyAccessor()
{
if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccess')) {
throw new RuntimeException('Unable to use expressions with data path as the Symfony PropertyAccess component is not installed.');
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should not throw this exception in the constraint to allow to detect this mistake earlier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course we could throw it there. But than we need to check there if dataPath is set because only then we check if the PropertyAccess is avaibale

Copy link
Member

Choose a reason for hiding this comment

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

I think that would be fine.

@nicolas-grekas
Copy link
Member

@xabbuh how should we move on here? 4.1?

@@ -11,6 +11,7 @@ CHANGELOG
the `Url::CHECK_DNS_TYPE_*` constants values and will throw an exception in Symfony 4.0
* added min/max amount of pixels check to `Image` constraint via `minPixels` and `maxPixels`
* added a new "propertyPath" option to comparison constraints in order to get the value to compare from an array or object
* added a `dataPath` option to the `Expression` constraint to allow an other way to get "this" from the context
Copy link
Member

Choose a reason for hiding this comment

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

another

Copy link
Member

Choose a reason for hiding this comment

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

"this" should be enclosed with backticks


private function getPropertyAccessor()
{
if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccess')) {
Copy link
Member

Choose a reason for hiding this comment

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

we should use the class constant instead

@@ -46,6 +47,10 @@ public function validate($value, Constraint $constraint)
$variables['value'] = $value;
$variables['this'] = $this->context->getObject();
Copy link
Member

Choose a reason for hiding this comment

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

to improve readability I would make this line an else branch of the if below

@xabbuh
Copy link
Member

xabbuh commented Oct 8, 2017

For me, if @Engerim could address my last comments very soon this could be merged into 3.4. Otherwise, I would vote for 4.1 too.

@Engerim
Copy link
Contributor Author

Engerim commented Oct 9, 2017

I work on this at the moment

Alexander Miehe added 2 commits October 9, 2017 09:53
* moved check if property access component is installed to constraint
* improve readability
@Engerim
Copy link
Contributor Author

Engerim commented Oct 9, 2017

the failing test has nothing to do with the changes i made

@@ -44,7 +45,12 @@ public function validate($value, Constraint $constraint)

$variables = array();
$variables['value'] = $value;
$variables['this'] = $this->context->getObject();

if (null !== $constraint->dataPath && '' !== $constraint->dataPath) {
Copy link
Member

Choose a reason for hiding this comment

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

if ($constraint->dataPath) { should be better


private function getPropertyAccessor()
{
return PropertyAccess::createPropertyAccessor();
Copy link
Member

Choose a reason for hiding this comment

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

the exception in src/Symfony/Component/Validator/Constraints/Expression.php should be moved here IMHO:
since the property is public, the check in the constructor is very fragile, only here would be robust

Copy link
Member

Choose a reason for hiding this comment

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

I would rather make the property private then. Catching these mistakes when the constraint is created allows to spot issues earlier which improves DX.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made the property private and added a magic __get like it was done in the File Constraint.

@@ -65,4 +71,9 @@ private function getExpressionLanguage()

return $this->expressionLanguage;
}

private function getPropertyAccessor()
Copy link
Member

Choose a reason for hiding this comment

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

IMO we don't need this method anymore and can inline the createPropertyAccessor() call instead.

Alexander Miehe added 2 commits October 9, 2017 11:34
* made dataPath private
* made dataPath protected so test is not failing
@Engerim
Copy link
Contributor Author

Engerim commented Oct 9, 2017

I don't know why windows is failing. This is not a problem from my changes

$variables['this'] = $this->context->getObject();

if ($constraint->dataPath) {
$variables['this'] = PropertyAccess::createPropertyAccessor()->getValue($this->context, $constraint->dataPath);
Copy link
Member

@yceruto yceruto Oct 9, 2017

Choose a reason for hiding this comment

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

Still I think we can get rid of dataPath altogether and set the root context directly for this case. Even, if the root is a form instance then we could set the form data.

Copy link
Member

Choose a reason for hiding this comment

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

How would you do that if the data is not available at the time you configure the constraint (which by the way is almost always the case when not using PHP to set up your constraints)?

Copy link
Member

@yceruto yceruto Oct 9, 2017

Choose a reason for hiding this comment

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

I'm not sure to understand your question :( but in short this is what I meant:

if (null === $variables['this'] = $this->context->getObject()) {
    if (($root = $this->context->getRoot()) instanceof FormInterface) {
        $variables['this'] = $root->getData();
    } else {
        $variables['this'] = $root;
    }
}

This will work with the example of the PR description and even for cases where there is data_class and the expression is setting over form fields.

Copy link
Member

Choose a reason for hiding this comment

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

But this would force the Validator component to know something about the Form component. I don't think that's a good idea. There may also be other similar issues (not related to Symfony) which we couldn't fix here.

Copy link
Member

@yceruto yceruto Oct 9, 2017

Choose a reason for hiding this comment

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

Right, then just:

$variables['this'] = $this->context->getObject() ?: $this->context->getRoot();

?

Copy link
Member

@yceruto yceruto Oct 9, 2017

Choose a reason for hiding this comment

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

IMHO there is no need to configure a dataPath if we have already a data root that we know, why not start from it directly?

@nicolas-grekas nicolas-grekas modified the milestones: 3.4, 4.1 Oct 12, 2017
@nicolas-grekas
Copy link
Member

Moving to 4.1.

@ostrolucky
Copy link
Contributor

ostrolucky commented Dec 17, 2017

I'm 👎 for this solution, because if this option is used in conjunction with form which have data binded, this value will be replaced with something different (in this case Form instance). Maybe just consider to exposecontext, or root variable in expression?

Also, this is really "fix" for #22403, rather than #12315

For more proper fix of #12315 see #25529

Also #25529 can be used with a trick to get out of this situation in user space. Something like this:

$obj = new \stdClass();

$builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $f) use ($obj) {
    $obj->form = $f->getForm();
});
...
new Expression([
  'expression' => 'obj.form["dateEnd"] >= obj.form["dateStart"]', 
  'variables' => ['obj' => $obj],
]);

Similar solution could be built into Form component.

fabpot added a commit that referenced this pull request Jan 8, 2018
…sion validator (ostrolucky)

This PR was merged into the 4.1-dev branch.

Discussion
----------

[Validator] Add option to pass custom values to Expression validator

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

I needed this in a Form. I had no way to pass things from `$options` into Expression validator.

Maybe can aid in #23134

Commits
-------

ba0565e [Validator] Add option to pass custom values to Expression validator
@fabpot
Copy link
Member

fabpot commented Feb 7, 2018

Closing as #25529 was merged.

@fabpot fabpot closed this Feb 7, 2018
@Engerim Engerim deleted the fix-issue-12315 branch February 8, 2018 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants