Problem/Motivation

ComponentElementAlter is looping on each prop and applying the normalization:

    foreach ($element["#props"] as $prop_id => $prop) {
      if (!isset($props[$prop_id])) {
        continue;
      }
      $prop_type = $props[$prop_id]['ui_patterns']['type_definition'];
      $element["#props"][$prop_id] = $prop_type->normalize($prop);
    }

However, attributes is not processed when not defined in the component definition (which is the usual case, because it is magically added by SDC).

Like variant with VariantPropType, attributes has always the same type: AttributesPropType

Proposed resolution

2 proposals

Early alteration in ComponentPluginManager::annotateProps()

$definition['props']['properties']['attributes'] = $this->buildAttributesProp($definition);

Doing that, attributes will be considered as a normal prop, automatically show up everywhere there are props (component library, form builder...). A bit like what variant do.

This will also fix #3469791: [2.0.0-beta2] Expose default attributes prop in forms

(If we hide it from component library in this MR, let's also do that for "variant")

Late alteration in ComponentElementAlter

Tested:

    if (isset($element["#props"]["attributes"])) {
      $element["#props"]["attributes"] = AttributesPropType::normalize($element["#props"]["attributes"]);
    }     

Doing that, we keep attributes special and we only alter the specific thing we need to fix.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

pdureau created an issue. See original summary.

pdureau credited g4mbini.

pdureau’s picture

Assigned: pdureau » Unassigned
Status: Active » Needs review

Let's do the early alteration in ComponentPluginManager::annotateProps().

#3469791: [2.0.0-beta2] Expose default attributes prop in forms is now obsolete and can be closed

pdureau’s picture

Assigned: Unassigned » pdureau
Status: Needs review » Needs work

We have this PHP Unit error in the pipeline: Drupal\Core\Render\Component\Exception\InvalidComponentException: [attributes] String value found, but an object is required

It may be related to #3469788: [2.0.0-beta2] Check PropTypeInterface::normalize()

pdureau’s picture

Checked with Mikael

It is SDC's ComponentValidator::validateProps() which is calling a dependency method which is executing json_encode() which is casting the attribute object into a string.

So, we may need 2 different methods:

  • keep PropTypeInterface::normalize() before the ComponentValidator, which is always returning data conform to JSON Schema, so which is keeping the associative array for attributes
  • add a new method (PropTypeInterface::preprocess() ?) in between the ComponentValidator and the injection to Twig template, which is instantiating the Attribute object

Unfortunately, ComponentValidator may be executed very late, from a Twig extension, so we may need to add our own Twig extension to be executed after.

pdureau’s picture

SDC implementation looks a bit messy IMHO:

ComponentsTwigExtension is adding 2 twig functions:

  • add_component_context which is adding the attribute Attribute object if missing
  • validate_component_props which is executing the JSON schema validator

Why are they Twig functions instead of living in the rendering process? Who will ever use them in the template?

Because ComponentNodeVisitor is printing those Twig functions on every template, which will be executed on rendering in this order:

  1. attach_library
  2. add_component_context
  3. validate_component_props

And this is making all the mess. What we are paying here is the cost of the use of "template to template" Twig functions or tags (include and embed) instead of a dedicated mechanism which is loading the Render API and leveraging the render element. For example, attach_library is used here because #attached is not executed.

This is problematic, but kind of works just because of another weird hack in the ComponentValidator to prevent the validation of attributes:

    // Remove the non JSON Schema types for validation down below.
    $definition['props'] = $this->nullifyClassPropsSchema(
      $schema,
      $classes_per_prop
    );

But this is the root cause of our current issue. This would need to be removed and fully redone, but it will take time and it may be too late.

What can we do now, in this humble ticket, to overcome this?

pdureau’s picture

So, let's see every possible situation.

attribute as a PHP object in template context

add_component_context is altering the Attribute object.

  • If attribute is missing from definition or is defined as a namespaced PHP class, validate_component_props is skipping the validation, so it is OK ✅
  • If attribute is defined as a proper JSON schema type (the UI Patterns 2.x way), validate_component_props is doing the validation, so is rendering the Attribute object, so we have [attributes] String value found, but an object is required

This is the current situation:

No attribute in template context

The same as before because add_component_context is adding the missing Attribute object, which is rendered as a string by the validator.

attribute as a PHP array in template context

This will be the situation if we remove AttributePropType::normalize()

add_component_context is doing nothing

  • If attribute is missing from definition or is defined as a namespaced PHP class, validate_component_props is skipping the validation, so it is OK ✅
  • If attribute is defined oas a proper JSON schema type (the UI Patterns 2.x way), validate_component_props is doing the validation, and it must be OK ✅

However, we are not sending a PHP object any more. ❌

Proposal

So, for the [attributes] String value found, but an object is required issue, the solution would be to:

  • Keep the attributes defined as proper JSON type instead of namespaced PHP classes
  • Remove the part creating Attribute PHP object in LinksPropType::normalize() and AttributesPropType::normalize() AND don't create Attribute PHP object in ComponentElementAlter::processAttributes()
  • Automatically inject an associative array in ComponentElementAlter::processAttributes() if no $element["#props"]["attributes"]) in order to not trigger the similar mechanism in add_component_context

in order to keep sending a PHP object instead of a PHP array in templates:

  • Add PropTypeInterface::preprocess() and implement it in LinksPropType and AttributesPropType
  • Add a ComponentNodeVisitor with a ModuleNode's leaveNode event, executed after the SDC one
  • Unfortunately, we may need to also create a Twig function to comply with SDC weirdness

The twig function would do something like that that:

    $props = $component->metadata->schema['properties'] ?? [];
    foreach ($context as $variable => $value) {
      if (!isset($props[$variable])) {
        continue;
      }
      $prop_type = $props[$variable]['ui_patterns']['type_definition'];
      $context][$variable] = $prop_type->preprocess($value);
    }
pdureau’s picture

Title: [2.0.0-beta2] ComponentElementAlter: normalize attributes » [2.0.0-beta2] Attributes normalization
pdureau’s picture

Assigned: pdureau » Unassigned
Status: Needs work » Needs review

pdureau’s picture

Status: Needs review » Fixed
pdureau’s picture

Status: Fixed » Closed (fixed)