Problem/Motivation

See #2927746-62: Update Symfony components to 3.4.*

Proposed resolution

Fix our usages in validation layer
Remove deprecation from whitelist in the deprecation bridge listener.

Remaining tasks

User interface changes

API changes

Data model changes

CommentFileSizeAuthor
#44 2937542-choice-44.patch11.83 KBlarowlan
#44 interdiff-2937542-choice-44.txt883 byteslarowlan
#42 interdiff.2937542.40-42.txt749 bytesmikelutz
#42 2937542-42.drupal.Not-setting-the-strict-option-of-the-Choice-constraint-to-true-is-deprecated-since-Symfony-34-and-will-throw-an-exception-in-40.patch11.64 KBmikelutz
#40 2937542-2-40.patch11.64 KBalexpott
#40 38-40-interdiff.txt807 bytesalexpott
#38 2937542-2-38.patch11.62 KBalexpott
#38 37-38-interdiff.txt1.08 KBalexpott
#37 2937542-2-37.patch11.52 KBalexpott
#37 34-37-interdiff.txt1.97 KBalexpott
#34 2937542-2-34.patch11.52 KBalexpott
#34 33-34-interdiff.txt839 bytesalexpott
#33 2937542-2-33.patch11.27 KBalexpott
#33 31-33-interdiff.txt6.85 KBalexpott
#31 interdiff.2937542.30-31.txt890 bytesmikelutz
#31 2937542-31.drupal.Not-setting-the-strict-option-of-the-Choice-constraint-to-true-is-deprecated-since-Symfony-34-and-will-throw-an-exception-in-40.patch12.39 KBmikelutz
#30 reset.patch12.34 KBmikelutz
#20 interdiff-2937542.txt3.31 KBdawehner
#20 2937542-20.patch8.53 KBdawehner
#19 2937542-19.patch8.28 KBalexpott
#19 16-19-interdiff.txt692 bytesalexpott
#16 2937542-16.patch8.24 KBalexpott
#16 12-16-interdiff.txt3.6 KBalexpott
#12 2937542-11.patch4.64 KBalexpott
#12 10-11-interdiff.txt2.27 KBalexpott
#10 2937542-10.patch3.28 KBalexpott
#10 9-10-interdiff.txt1.68 KBalexpott
#9 2937542-9.patch3.32 KBalexpott
#9 8-9-interdiff.txt2.18 KBalexpott
#8 2937542-8.patch3.36 KBalexpott
#4 2937542_4.patch1.9 KBMile23
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

larowlan created an issue. See original summary.

larowlan’s picture

Issue summary: View changes

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Mile23’s picture

Version: 8.6.x-dev » 8.7.x-dev
Status: Active » Needs review
FileSize
1.9 KB

Naively setting $strict = TRUE on our Choice subclass.

Mile23’s picture

Status: Needs review » Needs work

The last submitted patch, 4: 2937542_4.patch, failed testing. View results

larowlan’s picture

alexpott’s picture

Status: Needs work » Needs review
FileSize
3.36 KB

So let's see if typed data offers us a way out...

alexpott’s picture

That's not going to work :( Because \Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem::getSettableValues does not return boolean values but getCastedValue()
on it's main property would.

alexpott’s picture

We need the early return before the type safety.

One thought is do we need to bring

        if ($constraint->callback) {
            if (!\is_callable($choices = array($this->context->getObject(), $constraint->callback))
                && !\is_callable($choices = array($this->context->getClassName(), $constraint->callback))
                && !\is_callable($choices = $constraint->callback)
            ) {
                throw new ConstraintDefinitionException('The Choice constraint expects a valid callback');
            }
            $choices = \call_user_func($choices);
        } else {
            $choices = $constraint->choices;
        }

out of the parent implementation so we can do the same type juggling for callbacks. I think we probably do.

The last submitted patch, 8: 2937542-8.patch, failed testing. View results

alexpott’s picture

Addressing #10

The last submitted patch, 9: 2937542-9.patch, failed testing. View results

catch’s picture

Priority: Major » Critical
Issue tags: +Drupal 9, +Symfony 4

Bumping to critical since it's a hard Drupal 9 blocker.

alexpott’s picture

  1. +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
    @@ -47,9 +49,20 @@ public function __construct(AccountInterface $current_user) {
    +    if ($constraint->callback) {
    +      if (!\is_callable($choices = [$this->context->getObject(), $constraint->callback])
    +        && !\is_callable($choices = [$this->context->getClassName(), $constraint->callback])
    +        && !\is_callable($choices = $constraint->callback)
    +      ) {
    +        throw new ConstraintDefinitionException('The AllowedValuesConstraint constraint expects a valid callback');
    +      }
    +      $allowed_values = \call_user_func($choices);
    +      // parent::validate() does not need to invoke the callback again.
    +      $constraint->callback = NULL;
    +    }
    

    It would be good to have test coverage of this move and it's ability to affect types. This change is not required by core but very much might be needed by contrib / custom.

  2. +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
    @@ -69,6 +82,22 @@ public function validate($value, Constraint $constraint) {
    +      // Make the types match for strict checking. We can't use typed data here
    +      // because types are not enforced everywhere. For example,
    +      // \Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem::getSettableValues()
    +      // uses 0 and 1 to represent possible Boolean values whilst
    +      // \Drupal\Core\TypedData\Plugin\DataType\BooleanData::getCastedValue()
    +      // will return a proper typed Boolean. Therefore assume that
    +      // $allowed_values contains values of the same type and cast $value to
    +      // match.
    +      settype($value, gettype(reset($allowed_values)));
    

    This code is not the prettiest but actually solving the "TypedData not being properly typed" issue is of way bigger scope.

alexpott’s picture

Here's a test the proves the callback takes precedence to the allowed values and our type coercing works.

catch’s picture

+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
@@ -69,6 +82,22 @@ public function validate($value, Constraint $constraint) {
 
+    if (isset($allowed_values)) {
+      $constraint->choices = $allowed_values;
+      // Make the types match for strict checking. We can't use typed data here
+      // because types are not enforced everywhere. For example,
+      // \Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem::getSettableValues()
+      // uses 0 and 1 to represent possible Boolean values whilst
+      // \Drupal\Core\TypedData\Plugin\DataType\BooleanData::getCastedValue()
+      // will return a proper typed Boolean. Therefore assume that
+      // $allowed_values contains values of the same type and cast $value to
+      // match.
+      settype($value, gettype(reset($allowed_values)));
+    }
+    elseif ($typed_data instanceof PrimitiveInterface) {
+      $value = $typed_data->getCastedValue();
+    }
+

OK this is really tricky but the comment explains what's going on well enough.

My only question is why not copy over the Symfony 3 non-strict logic that's been deprecated - would that be more/worse code to maintain than what we're adding here?

larowlan’s picture

+++ b/core/tests/Drupal/KernelTests/Core/TypedData/AllowedValuesConstraintValidatorTest.php
@@ -49,6 +50,61 @@ public function testValidation() {
+   * @return array

do we need a comment here

Other than that, this makes sense to me

alexpott’s picture

@catch I think it is worth doing this because in_array() without strict checking is really hard to predict how it works - there are just so many gotchas. Also this is moving in the direction we should be going with TypedData being typed and using PHP 7's primitive types. So it's a stepping stone. It also keeps us in-line with Symfony so when we doing get round to strciter typing in TypedData we've not got so far to move.

@larowlan we never really agreed a loosening of coding standards for tests - so fixed. One day we'll open a policy issue about that :)

dawehner’s picture

While reviewing the code I was wondering whether we could reuse the existing parent's code by setting the callback, but yeah it is not. I added a comment about this.
On top of that I adopted the tests to not add additional deprecated assertion calls.

catch’s picture

Status: Needs review » Reviewed & tested by the community

I don't think there's anything else left to do here.

mikelutz’s picture

I've been reviewing this along with other issues in the constraints system for symfony 4. I've been testing it in conjunction with some other patches around the constraint system, and it is working well. The approach makes since given the, ahem, 'constraints' we are facing.

+1 on RTBC

  • larowlan committed 030a9ce on 8.7.x
    Issue #2937542 by alexpott, dawehner, Mile23, catch, mikelutz: Not...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed 030a9ce and pushed to 8.7.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Chi’s picture

Technically it is a BC break. Existing code may rely on non-strict validation.
https://www.drupal.org/pift-ci-job/1200662

If we desperately need this we should at least provide a change record to explain how custom code is supposed to be updated.

Chi’s picture

mikelutz’s picture

+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
@@ -69,6 +86,22 @@ public function validate($value, Constraint $constraint) {
+      // Make the types match for strict checking. We can't use typed data here
+      // because types are not enforced everywhere. For example,
+      // \Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem::getSettableValues()
+      // uses 0 and 1 to represent possible Boolean values whilst
+      // \Drupal\Core\TypedData\Plugin\DataType\BooleanData::getCastedValue()
+      // will return a proper typed Boolean. Therefore assume that
+      // $allowed_values contains values of the same type and cast $value to
+      // match.
+      settype($value, gettype(reset($allowed_values)));
+    }

Floats can't be array keys, so list_float typecasts keys to a mix of integers or strings. If a float options allowed value list is [0.0, 1.5, 2.0], then the allowed values list is [(int)0, "1.5", (int)2] if we check a value of "1.5" it is being typecast to gettype(0) = (int)1.5 = 1 before being validated and fails.

Gábor Hojtsy’s picture

Status: Closed (fixed) » Needs work

I rolled this back based on discussion today with @mikelutz. For one the solution to the problem is not trivial. Second, it looks like a redo of the patch and not really a followup. Better to not have -dev include known regressions that we cannot fix quickly. @mikelutz will bring the fix back here from #3033120: [Regression] Options select widget does not work when allowed values are a mix of strings and integers.

mikelutz’s picture

mikelutz’s picture

mikelutz’s picture

Okay, so this should work with some cleanup. Basically default AllowedValueConstraint::strict to FALSE, set it to true for the core use cases and handle the typing for Booleans and Integer lists. If we get through there, and strict still hasn't been set to true, then check if the validator passes for non-strict but not strict, and if it does, throw a deprecation error, typecast so it works with strict and send it on its way.

This way we preserve non-strict behavior behind a deprecation error, and take care of the core use cases.

I'll try to clean up the patch and the direct testing tomorrow.

alexpott’s picture

Status: Needs work » Needs review
FileSize
6.85 KB
11.27 KB

Here's another approach - kinda going back to #8 but doing an additional couple of twists. Doing the callback ourselves so we can later set the type of the allowed values using the typed data we've been provided. This patch includes the new tests added by @mikelutz to prove what we broke in #20 that was committed and then reverted.

alexpott’s picture

@mikelutz pointed out I was miss a use - now I've confirmed in debug that when dealing with ListFloatItem we are casting all the values to floats properly.

alexpott’s picture

For those wondering why #33 is green - it's because all the things were cast to string because the instanceof PrimitiveInterface always returned false.

Status: Needs review » Needs work

The last submitted patch, 34: 2937542-2-34.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
1.97 KB
11.52 KB

So the patch in #34 broke

    // The parent implementation ignores values that are not set, but makes
    // sure some choices are available firstly. However, we want to support
    // empty choices for undefined values; for instance, if a term reference
    // field points to an empty vocabulary.
    if (!isset($value)) {
      return;
    }

because NULL was being cast to an empty string. So to fix we need to do the casting after this check. Which funnily enough makes the patch a little bit nicer in my opinion.

alexpott’s picture

+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
@@ -69,6 +73,34 @@ public function validate($value, Constraint $constraint) {
+    // Get the value with the proper datatype in order to make strict
+    // comparisons using in_array().
+    if ($typed_data instanceof PrimitiveInterface) {
+      $value = $typed_data->getCastedValue();
+    }

So one thought I have is are we not always dealing with a PrimitiveInterface type at this point and what would it mean if we're not.

Uploading a patch to find out what's happening in HEAD.

mikelutz’s picture

+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
@@ -69,6 +73,35 @@ public function validate($value, Constraint $constraint) {
+    foreach ($constraint->choices as &$choice) {
+      settype($choice, gettype($value));
+    }

I suppose we should evaluate gettype($value) outside of the loop.

alexpott’s picture

Good idea @mikelutz. I tried to make this bit of code more elegant with array_map / array_walk but nothing looked nicer and as easier to grok as the foreach to me.

Status: Needs review » Needs work

The last submitted patch, 40: 2937542-2-40.patch, failed testing. View results

mikelutz’s picture

I had ran a speed test earlier, and foreach ran the fastest anyway. array_map was fine when I was only doing it for ListFloatItem and was casting to float, but I think the foreach is cleaner than trying to deal with getting the type in the closure.

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

Looks good me to me as well. Thanks for working out a better solution.

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
883 bytes
11.83 KB

Because we're doing type-casting here, I'd like to see a test where we have string allowed values and test with 0 as the value.

Added that.

Status: Needs review » Needs work

The last submitted patch, 44: 2937542-choice-44.patch, failed testing. View results

Gábor Hojtsy’s picture

Status: Needs work » Reviewed & tested by the community

Superb, thanks for the additional test coverage :)

larowlan’s picture

Adding credit for Chi who caught the regression

larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed c9dd655 and pushed to 8.7.x. Thanks!

My patch only added an extra test, so I think I'm still eligible to commit this.

larowlan’s picture

Cherry-picked this to 8.8 as the branch is now open and updated my commit tooling to commit to 8.8.x first from here out.

  • larowlan committed c9dd655 on 8.7.x
    Issue #2937542 by alexpott, mikelutz, larowlan, dawehner, Mile23, catch...

  • larowlan committed 8cd35db on 8.8.x
    Issue #2937542 by alexpott, mikelutz, larowlan, dawehner, Mile23, catch...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.