Unhelpful PHP error with NULL type property

Created on 17 July 2024, 6 months ago

Problem/Motivation

The simple component with null type (without wrapping it in a string) produces an unhelpful PHP error.

TypeError: Drupal\Core\Theme\Component\ComponentValidator::Drupal\Core\Theme\Component\{closure}(): Argument #1 ($type) must be of type string, null given in Drupal\Core\Theme\Component\ComponentValidator::Drupal\Core\Theme\Component\{closure}() (line 278 of core/lib/Drupal/Core/Theme/Component/ComponentValidator.php).

Steps to reproduce

Create a component propert with type null:

$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: Example
status: experimental
props:
  type: object
  properties:
    foo:
      type: [null, boolean]

It should be 'null', but if you miss that part and get back to component usage later, it is hard to understand which component is the problematic one, because the error doesn't have any hints to it.

  private function getClassProps(array $props_schema): array {
    $classes_per_prop = [];
    foreach ($props_schema['properties'] ?? [] as $prop_name => $prop_def) {
      $type = $prop_def['type'] ?? 'null';
      $types = is_string($type) ? [$type] : $type;
      // For each possible type, check if it is a class.
      $class_types = array_filter($types, static fn(string $type) => !in_array(
        $type,
        ['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']
      ));
      $classes_per_prop[$prop_name] = $class_types;
    }
    return array_filter($classes_per_prop);
  }

This is the current code from \Drupal\Core\Theme\Component\ComponentValidator::getClassProps. As you can see, the anonymous function expects $type to be a strictly string. But if it is NULL, this leads to a PHP error with an useless message.

$type = $prop_def['type'] ?? 'null'; β€” this part also fails, because it is an array with a NULL value. https://3v4l.org/lfJ6e

Proposed resolution

  1. Add fool's protection and fix it for the developer by casting NULL into 'null'.
  2. Validate types before it enters \Drupal\Core\Theme\Component\ComponentValidator::getClassProps and throw a meaningful exception with mention of component ID and problem.

Remaining tasks

Decide how to handle this. The loop below also expects that all props will be strings, so maybe there should be a validation before that, to ensure that all property types are actual strings?

This is an edge case, but still causing same error:

$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: Example
status: experimental
props:
  type: object
  properties:
    foo:
      type: [!!float 12.3, boolean]
πŸ“Œ Task
Status

Active

Version

11.0 πŸ”₯

Component
single-directory componentsΒ  β†’

Last updated about 8 hours ago

Created by

πŸ‡·πŸ‡ΊRussia niklan Russia, Perm

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @niklan
  • Merge request !8807Add test with broken property type. β†’ (Closed) created by niklan
  • Status changed to Needs review 6 months ago
  • πŸ‡·πŸ‡ΊRussia niklan Russia, Perm

    Added the test that will fail with such definition. It is practically useless, I think, but it shows that the input and expected data are not the same. If you try to render it, a PHP error occurs. Maybe we should throw an exception in that case, I don't know.

  • Pipeline finished with Failed
    6 months ago
    #226998
  • Pipeline finished with Canceled
    6 months ago
    Total: 65s
    #227001
  • Pipeline finished with Failed
    6 months ago
    Total: 578s
    #227003
  • πŸ‡·πŸ‡ΊRussia niklan Russia, Perm

    I propose to throw an exception if the types of the property contain values that are not strings, which will include floats and special YAML types. We definitely do not want to correct the value !!float 12.3 in any way. We should throw a meaningful exception, indicating the component ID and property name. Then developers can easily identify and fix the problem.

    If we are going to fix NULL for developers automatically, we still have to check for other types and throw an exception. So it's just more code with basically the same result.

  • πŸ‡·πŸ‡ΊRussia niklan Russia, Perm

    Added check for property type definition values, and if it contains a non-string value, throws an exception. In my case, it is now clear where the problem is:

    Drupal\Core\Render\Component\Exception\InvalidComponentException: The component "foo:bar" uses non-string types for properties: required. in Drupal\Core\Theme\Component\ComponentValidator->validateDefinition() (line 96 of core/lib/Drupal/Core/Theme/Component/ComponentValidator.php).

    $schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
    name: Bar
    props:
      type: object
      properties:
        required:
          type: [null, boolean]
          title: Required
    
  • Pipeline finished with Success
    6 months ago
    Total: 632s
    #228072
  • πŸ‡·πŸ‡ΊRussia niklan Russia, Perm
  • Status changed to RTBC 6 months ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Tried to get some feedback in the slack channel #components but no luck.

    Believe this is a net improvement for sure though

    Test-only feature shows that

    1) Drupal\Tests\Core\Theme\Component\ComponentValidatorTest::testValidateDefinitionInvalid with data set #3 (['https://git.drupalcode.org/pr...a.json', 'Call to Action', 'My description', ['object', ['text'], [[null, 'Title', 'The title for the cta', ['Press', 'Submit now']], ['string', 'URL', 'uri'], ['string', 'Target', ['', '_blank']], ['Drupal\Core\Template\Attribute', 'Attributes']]], 'my-cta', 'module', 'sdc_test:my-cta', [[[[]]]], '', 'sdc_test', 'my-cta.twig', 'my-group'])
    Failed asserting that exception of type "Drupal\Core\Render\Component\Exception\InvalidComponentException" is thrown.
    2) Drupal\Tests\Core\Theme\Component\ComponentValidatorTest::testValidateDefinitionInvalid with data set #4 (['https://git.drupalcode.org/pr...a.json', 'Call to Action', 'My description', ['object', ['text'], [[['string', null], 'Title', 'The title for the cta', ['Press', 'Submit now']], ['string', 'URL', 'uri'], ['string', 'Target', ['', '_blank']], ['Drupal\Core\Template\Attribute', 'Attributes']]], 'my-cta', 'module', 'sdc_test:my-cta', [[[[]]]], '', 'sdc_test', 'my-cta.twig', 'my-group'])
    Failed asserting that exception of type "TypeError" matches expected exception "Drupal\Core\Render\Component\Exception\InvalidComponentException". Message was: "Drupal\Core\Theme\Component\ComponentValidator::Drupal\Core\Theme\Component\{closure}(): Argument #1 ($type) must be of type string, null given" at
    /builds/issue/drupal-3462156/core/lib/Drupal/Core/Theme/Component/ComponentValidator.php:278
    /builds/issue/drupal-3462156/core/lib/Drupal/Core/Theme/Component/ComponentValidator.php:84
    /builds/issue/drupal-3462156/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php:62
    
  • Status changed to Needs work 5 months ago
  • πŸ‡³πŸ‡ΏNew Zealand quietone

    I don't see that anyone has received the code for this issue, only that the test only feature has been used. Remember, issues do need a code review. I thought at first that this was throwing a new exception but I see that it is not. I do think a comment is needed, which I indicated in the MR, perhaps some of the detail in #4 would be useful there.

    The title here should include some indication of what this fix applies to. The title should be a description of what is being fixed or improved. The title is used as the git commit message so it should be meaningful an concise. See List of issue fields β†’ .

    So, two small things, although this still needs a review by someone familiar with this component.

  • Pipeline finished with Canceled
    5 months ago
    Total: 87s
    #254990
  • Status changed to Needs review 5 months ago
  • πŸ‡·πŸ‡ΊRussia niklan Russia, Perm

    Added comment and adjusting issue title

  • Pipeline finished with Failed
    5 months ago
    #254991
  • Status changed to RTBC 5 months ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Believe title reflects the change so removing that tag.

    Also appears additional feedback has been addressed

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    I think the test could be slightly clearer (see MR comment), but I think this is ready nonetheless πŸ‘

  • Pipeline finished with Success
    5 months ago
    Total: 602s
    #258231
  • Status changed to Needs work 4 months ago
  • πŸ‡³πŸ‡ΏNew Zealand quietone

    Changing to NW to implement the suggested changes to the dataprovider.

  • Pipeline finished with Failed
    4 months ago
    Total: 126s
    #286322
  • Pipeline finished with Failed
    4 months ago
    Total: 480s
    #286323
  • Pipeline finished with Canceled
    4 months ago
    #286332
  • Pipeline finished with Success
    4 months ago
    Total: 405s
    #286333
  • Status changed to Needs review 4 months ago
  • πŸ‡·πŸ‡ΊRussia niklan Russia, Perm

    Updated MR and improved data provider.

    P.S. Sorry for the delay, I did not receive notification from MR's comments.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave
     * @return array
       *   The data.

    Shouldn't this be updated now too?

  • First commit to issue fork.
  • πŸ‡¬πŸ‡·Greece vensires

    I have addressed the comment in #14 from @smustgrave.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    believe feedback to be addressed

  • πŸ‡³πŸ‡ΏNew Zealand quietone

    All questions are answered here. I didn't do a code review, I am a bit too tired.

    Leaving at RTBC

    • larowlan β†’ committed f0b4dd2b on 11.x
      Issue #3462156 by niklan, vensires, smustgrave, quietone, wim leers:...
  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    Committed to 11.x - thanks folks.

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

  • Status changed to Fixed 16 days ago
  • πŸ‡ΊπŸ‡ΈUnited States mherchel Gainesville, FL, US

    I'm running into an issue caused by this fix. In many components, I've allowed multiple types. Example:

      properties:
        is_active:
          type:
            - null
            - boolean

    Now, I can't see how to do this. Furthermore, I've written code that's in prod that does this, but there's no change record. When prod updates to 11, this will break.

  • πŸ‡·πŸ‡ΊRussia niklan Russia, Perm

    Hi, it should be:

      properties:
        is_active:
          type:
            - 'null'
            - boolean
    

    Other types are basically cast to a string by the YML parser, and boolean becomes 'boolean' internally as any other type because they are both strings. However, the null type is special for YML, and it is not cast to anything. It just becomes a simple NULL. In fact, this has never worked, and your previous example should also have thrown an exception when you pass an actual NULL value.

    Previously, if a component could actually have a null value dynamically, and was mostly used for a fail-safe solution (for example, a field with data was empty, but you expected it to be present in most cases), it would fail in production without a clear understanding of why. This was a much more dangerous approach, which I faced multiple times. Without other patches ( ✨ Unhelpful error message with enumeration problem Active ), you will not even know which component has failed on the page. It is just a WSOD.

    As proposed initially, we could add some kind of protection by casting all null types to 'null' internally to improve the DX. Or we could simply add a note to the change record mentioning this. I am not sure which is better. I think we should get input from the core maintainers here.

    From a component developer's perspective, the requirement to wrap null into a string seems unnecessary, because other data types do not require that. This is what the exception added in that issue is all about - it highlights the problems and errors in the component that were previously hidden until a NULL value was passed and it was confusing why the component failed when the type was clearly listed. At least now, it triggers an exception before the code is deployed, but it looks like it's not clear what's actually wrong. Maybe we should improve the exception message by adding extra information about null.

    From the code developer's point of view, the component's validator is already difficult to understand, in my opinion. Adding such workarounds to automatically fix these issues will not solve the problem, and may even make it worse. Additionally, we are walking on a slippery slope, as there are likely other cases that are handled differently by the YML parser, and would require additional workarounds for those. I can't recall, but it's likely that if we try to work around this at the validator level it will be necessary in other parts of the component code, as the validator would only "fix it" for the validation process but it would still be null inside, and other parts of code might still fail. At the current stage, it detects problems very early and throws an exception, and then we don't worry about it much. It should either be fixed or deleted by the component's developer.

    As another suggestion, we could add a best practice/tip/hint/guideline to wrap all values in strings. This would make it consistent and safer. Internally, component's code expects all type values to be wrapped in strings, so it makes sense to do this. However, this would require wrapping everything except null. This is what it actually should be from the code perspective:

      properties:
        is_active:
          type:
            - 'null'
            - 'boolean'
    

    ^ and this is the "proper" example and usage, based on how YML parser will treat it.

Production build 0.71.5 2024