Change record status: 
Project: 
Introduced in branch: 
1.x
Introduced in version: 
1.1.0
Description: 

History/context

Multi-bundle expressions were introduced in #3530521: Decouple image shape matching from the `image` MediaType: support matching multiple bundles of a single MediaSources (`image`), to be able to support entity reference fields that referenced multiple bundles, but needed bundle-specific field names. Later, #3530533: Support image shape matching against multiple bundles of multiple MediaSources (`image` + `oembed` + `acquia_dam_asset` + …) expanded this to support different props per field name.

That worked fine for a while, but has one significant limitation: it only supports the case of per-bundle field names/field properties. But sometimes, one referenced bundle's field provides access to the necessary information directly (on a property of that field), whereas another's referenced bundle's field may itself be a reference field that needs to be followed.

So, a more expressive, long-term solution was devised: the ability to create per-bundle branches in reference prop expressions. (Want more detail? Jump to the details below!)

Site builders: update path

✅ Zero visible changes. No disruption. Just run the update path.

Existing components whose "image" and "video" props were using the old kind of expression:

  • Component config entities will be updated automatically: they'll gain a new version (visible at /admin/appearance/component
  • new component instances will use the updated expressions
  • existing component instances will continue to work and will also use the updated expressions, thanks to #3560005

Note: in #3463996: [META] When the field type, storage/instance settings, widget, expression or requiredness for an SDC/code component prop changes, the Content Creator must be able to upgrade, work is under way to allow updating existing component instances.

Module maintainers

Deprecations

For hook_canvas_storable_prop_shape_alter() implementations:

  1. Multi-bundle reference expressions specified in hook_canvas_storable_prop_shape_alter() implementations:
    @trigger_error('Creating ' . __CLASS__ . ' that contains references targeting multiple bundles is deprecated in canvas:1.1.0 and will be removed from canvas:2.0.0. Instead, create a ' . ReferenceFieldTypePropExpression::class . ', then use its ::withAdditionalBranch() to create multiple expression branches, each pointing to a single-bundle ' . __CLASS__ . '. See https://www.drupal.org/node/3563451', E_USER_DEPRECATED);
    
  2. Object expressions with each key-value pair using the same reference:
    @trigger_error('Creating ' . __CLASS__ . ' with the same reference for each object prop should is deprecated in canvas:1.1.0 and will be removed from canvas:2.0.0. Instead, create a ' . ReferenceFieldTypePropExpression::class . ' and point it to a ' . FieldObjectPropsExpression::class . '. See https://www.drupal.org/node/3563451', E_USER_DEPRECATED);
    

Notes:

Module maintainers that have hook_canvas_storable_prop_shape_alter() implementations

Before
Overwrite the entirety of what ShapeMatchingHooks::mediaLibraryStorablePropShapeAlter() did.
After
They now can instead use: ReferenceFieldTypePropExpression::withAdditionalBranch()!

(That's the 99% scenario. It's possible they want to use ::hasBranch() and ::withoutBranch().)

Illustrated (and quoting canvas.api.php):

function hook_canvas_storable_prop_shape_alter(CandidateStorablePropShape $storable_prop_shape): void {
  // The `type: object, $ref: json-schema-definitions://canvas.module/image`
  // shape allows picking any media of a media type powered by the "image" media
  // source by default.
  // Some sites may want to exclude certain media types, and/or add other media
  // types that use a different media source (with a different expression).
  // @see \Drupal\canvas\PropExpressions\StructuredData\ReferenceFieldTypePropExpression::hasBranch()
  // @see \Drupal\canvas\PropExpressions\StructuredData\ReferenceFieldTypePropExpression::withoutBranch()
  // @see \Drupal\canvas\PropExpressions\StructuredData\ReferenceFieldTypePropExpression::withAdditionalBranch()
  if (
    // "image" object shape?
    $storable_prop_shape->shape->schema['type'] === 'object'
    && isset($storable_prop_shape->shape->schema['$ref'])
    && $storable_prop_shape->shape->schema['$ref'] === 'json-schema-definitions://canvas.module/image'
    // Currently using media types?
    // @see \Drupal\canvas\Hook\ShapeMatchingHooks::mediaLibraryStorablePropShapeAlter()
    && $storable_prop_shape->fieldTypeProp instanceof ReferenceFieldTypePropExpression
    && $storable_prop_shape->fieldTypeProp->getFieldType() === 'entity_reference'
    && $storable_prop_shape->fieldTypeProp->getTargetExpression()->getHostEntityDataDefinition()->getEntityTypeId() === 'media'
  ) {
    $expr = $storable_prop_shape->fieldTypeProp;
    $target_bundles = $storable_prop_shape->fieldInstanceSettings['handler_settings']['target_bundles'];

    // Exclude the "vacation_photos" media type: don't allow it to be stored in
    // the field, and update the expression.
    if ($expr->hasBranch('entity:media:vacation_photos')) {
      $target_bundles = array_diff_key($target_bundles, array_flip(['vacation_photos']));
      $expr = $expr->withoutBranch('entity:media:vacation_photos');
    }

    // Add the "remote_image" media type, which uses the oEmbed media source.
    // Allow it to be stored in the field, and update the expression.
    // @see https://www.drupal.org/project/media_remote_image
    $target_bundles = $target_bundles + ['remote_image' => 'remote_image'];
    $expr->withAdditionalBranch(new FieldPropExpression(
      entityType: BetterEntityDataDefinition::create('media', ['remote_image']),
      fieldName: 'field_media_remote_image',
      delta: NULL,
      // @todo Update this to use the relevant computed property instead of "non_existent_computed_property" after Canvas depends on a Drupal core version that includes https://www.drupal.org/project/drupal/issues/3567249
      propName: 'non_existent_computed_property',
    ));

    // Apply the updated changes.
    $storable_prop_shape->fieldTypeProp = $expr;
    $storable_prop_shape->fieldInstanceSettings['handler_settings']['target_bundles'] = $target_bundles;
  }
}

For recipe maintainers

It is recommended to update any exported/default content that contains Canvas component trees to target the newest version of each of the Component config entities that are used by the instances in that component tree.

(That will reduce the amount of updates needed by #3463996: [META] When the field type, storage/instance settings, widget, expression or requiredness for an SDC/code component prop changes, the Content Creator must be able to upgrade.)

Details and rationale

The new per-bundle reference support can be slightly more verbose in the string representation, but is:

  • more explicit: depending on the bundle of the referenced entity, a different expression of arbitrary can be applied — previously this was only a specific field name and specific property name
  • a better conceptual fit: the branching point truly is the entity reference field, not just any field, so having the branching happen in a ReferenceField(Type)PropExpression is better than in a Field(Type)PropExpression
  • actually less verbose in a number of cases too (when populating an "object" shape — as illustrated in the before vs after example below)

Let's look at a concrete, representative example: an "image" prop (an object with a required src, optional alt, width and height) that is populated by a media reference pointing to either baby_photos image media or vacation_photos image media.

Before
1️⃣
ℹ︎entity_reference␟{src↝entity␜␜entity:media:baby_photos|vacation_photos␝field_media_image|field_media_image_1␞␟src_with_alternate_widths,alt↝entity␜␜entity:media:baby_photos|vacation_photos␝field_media_image|field_media_image_1␞␟alt,width↝entity␜␜entity:media:baby_photos|vacation_photos␝field_media_image|field_media_image_1␞␟width,height↝entity␜␜entity:media:baby_photos|vacation_photos␝field_media_image|field_media_image_1␞␟height} 

or formatted for readability:

ℹ︎entity_reference␟{
  src↝entity␜␜entity:media:baby_photos|vacation_photos␝field_media_image|field_media_image_1␞␟src_with_alternate_widths,
  alt↝entity␜␜entity:media:baby_photos|vacation_photos␝field_media_image|field_media_image_1␞␟alt,
  width↝entity␜␜entity:media:baby_photos|vacation_photos␝field_media_image|field_media_image_1␞␟width,
  height↝entity␜␜entity:media:baby_photos|vacation_photos␝field_media_image|field_media_image_1␞␟height
}

👆 Note the repetition of the 2 bundles, and each bundle's field name! (And this is not even using per-bundle field property names, then: those are the same across bundles.)

2️⃣ An even more complicated one, where some object props are not populated by some bundles:

ℹ︎entity_reference␟{src↝entity␜␜entity:media:baby_photos|image|remote_image␝field_media_image_1|field_media_image|field_media_test␞␟src_with_alternate_widths|src_with_alternate_widths|value,alt↝entity␜␜entity:media:baby_photos|image|remote_image␝field_media_image_1|field_media_image|field_media_test␞␟alt|alt|␀,width↝entity␜␜entity:media:baby_photos|image|remote_image␝field_media_image_1|field_media_image|field_media_test␞␟width|width|␀,height↝entity␜␜entity:media:baby_photos|image|remote_image␝field_media_image_1|field_media_image|field_media_test␞␟height|height|␀}

or formatted for readability:

ℹ︎entity_reference␟{
 src↝entity␜␜entity:media:baby_photos|image|remote_image␝field_media_image_1|field_media_image|field_media_test␞␟src_with_alternate_widths|src_with_alternate_widths|value
  alt↝entity␜␜entity:media:baby_photos|image|remote_image␝field_media_image_1|field_media_image|field_media_test␞␟alt|alt|␀
  width↝entity␜␜entity:media:baby_photos|image|remote_image␝field_media_image_1|field_media_image|field_media_test␞␟width|width|␀
  height↝entity␜␜entity:media:baby_photos|image|remote_image␝field_media_image_1|field_media_image|field_media_test␞␟height|height|␀
}
After
1️⃣
ℹ︎entity_reference␟entity␜[␜entity:media:baby_photos␝field_media_image␞␟{src↠src_with_alternate_widths,alt↠alt,width↠width,height↠height}][␜entity:media:vacation_photos␝field_media_image_1␞␟{src↠src_with_alternate_widths,alt↠alt,width↠width,height↠height}]
or formatted for readability:
ℹ︎entity_reference␟entity␜
  [␜entity:media:baby_photos␝field_media_image␞␟
    {
      src↠src_with_alternate_widths,
      alt↠alt,
      width↠width,
      height↠height
    }
  ]
  [␜entity:media:vacation_photos␝field_media_image_1␞␟
    {
      src↠src_with_alternate_widths,
      alt↠alt,
      width↠width,
      height↠height
    }
  ]

👆 Note the 2 clear branches, one per bundle, and for each the much simpler (albeit repetitive) expressions to populate the target object shape.

2️⃣ And the more complex one:
ℹ︎entity_reference␟entity␜[␜entity:media:baby_photos␝field_media_image_1␞␟{src↠src_with_alternate_widths,alt↠alt,width↠width,height↠height}][␜entity:media:image␝field_media_image␞␟{src↠src_with_alternate_widths,alt↠alt,width↠width,height↠height}][␜entity:media:remote_image␝field_media_test␞␟{src↠value}]
or formatted for readability:

ℹ︎entity_reference␟entity␜
  [␜entity:media:baby_photos␝field_media_image_1␞␟
    {
      src↠src_with_alternate_widths,
      alt↠alt,
      width↠width,
      height↠height
    }
  ]
  [␜entity:media:image␝field_media_image␞␟
    {
      src↠src_with_alternate_widths,
      alt↠alt,
      width↠width,
      height↠height
    }
  ]
  [␜entity:media:remote_image␝field_media_test␞␟
    {
      src↠value
    }
  ]

👆 Note the absence of ␀ (aka \Drupal\canvas\PropExpressions\StructuredData\StructuredDataPropExpressionInterface::SYMBOL_OBJECT_MAPPED_OPTIONAL_PROP): that has effectively become obsolete. Because when using branching expressions, it simply is not necessary to list the (non-required) object shape key-value pairs that some bundle cannot populate!

The above applies to field type-based reference expressions (\Drupal\canvas\PropExpressions\StructuredData\FieldTypeBasedPropExpressionInterface), which are the only ones contrib developers may interact with via hook_canvas_storable_prop_shape_alter().

But internally, the equivalent has also happened for entity field-based reference expressions (\Drupal\canvas\PropExpressions\StructuredData\EntityFieldBasedPropExpressionInterface), to allow populating component props in Canvas content templates from multi-target bundle entity reference fields. The typical scenario: a "media" field that allows picking images, remote images, Flickr images — aka different "media types", and powered by different "media source" plugins.
Finally, for entity field-base

Impacts: 
Site builders, administrators, editors
Module developers
Site templates, recipes and distribution developers