Problem/Motivation

FieldValue::walk(), getHopOptions(), isDescendable(), hopFieldOptions() and traversalLabel() total about 500 lines implementing a labeled, cycle-guarded walk down entity reference chains: discovering what is descendable, what its target types are, and how each hop should be named.

This is a solved problem with well-known edge cases — multi-target reference fields, computed fields, recursive types — and every one of them has to be rediscovered here.

Proposed resolution

Use Typed Data API's DataFetcher and property-path handling for the traversal itself, keeping this module's presentation on top: the -joined labeled hops and the target-aware root. typed_data is API-only and already arrives transitively with ECA, which is a suggestion, so the added weight is small.

Remaining tasks

  • Confirm typed_data's traversal covers what the current walk handles, notably multi-target reference fields and the descendable/not-descendable split.
  • Map property paths back to the stored path format.
  • Keep the stored condition format unchanged, or write the change up under Data model changes.

User interface changes

None intended. The hop selector should offer the same options with the same labels.

API changes

Internal to FieldValue. No change to SegmentPluginInterface or QueryableSegmentPluginInterface.

Data model changes

None intended. If typed_data's path syntax is adopted in storage rather than only internally, stored conditions change format and that must be called out here.

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

jdleonard created an issue. See original summary.

jdleonard’s picture

Assigned: Unassigned » jdleonard

jdleonard’s picture

Title: Use typed_data for entity reference traversal in the Field value plugin » Use core Typed Data for FieldValue reference traversal (via a property_traversal submodule)
Assigned: jdleonard » Unassigned
Status: Active » Fixed

Worked this in MR !6. The investigation changed the approach, so a few notes
for the record — including corrections to this issue's own summary.

typed_data (the proposed mechanism) was evaluated and set aside.
Two findings, verified against a real build:

  1. It does not arrive with ECA. No ECA branch (1.1.x, 2.0.x, 2.1.x, or the
    released 3.x) requires drupal/typed_data — it would be a genuinely
    new dependency, not a transitive one.
  2. Its definition traversal is lossy at every hop. DataFetcher
    drills via EntityDataDefinition::getPropertyDefinitions(), which
    falls back to getBaseFieldDefinitions() unless the definition is
    pinned to exactly one bundle — and core builds every entity-reference hop target
    bundle-less (EntityReferenceItem::propertyDefinitions()). So every
    hop would silently drop all configurable fields, which is most of what
    the selector needs to offer. Core carries a standing @todo for the
    multi-bundle case (#2169813).

What landed instead: the same idea — stand on the Typed Data
API instead of a bespoke walk — but on core Typed Data
interfaces, in the shape Search API's FieldsHelper uses. Its
getNestedProperties() is exactly the missing piece: it unions
getFieldDefinitions() across a hop target's bundles, over
ComplexDataDefinitionInterface /
DataReferenceDefinitionInterface /
ListDataDefinitionInterface. Search API itself doesn't depend on the
typed_data module, and neither does this. Net result: the goal is met
with no new Composer dependency.

The traversal now lives in a new property_traversal submodule
exposing a property_traversal.fetcher service
(PropertyTraversalInterface). FieldValue consumes it and
no longer walks references itself. Behaviour, hop-selector options/labels, and the
stored <ref>.entity.<field> format are unchanged.

Corrections to this issue's summary:

  • API changes — no longer "internal to FieldValue". This introduces
    a public service and a new submodule boundary.
    SegmentPluginInterface and
    QueryableSegmentPluginInterface are still untouched. The submodule
    is deliberately unprefixed and depends only on core (dependency runs
    entity_segment → property_traversal) so it can later move to its
    own project.
  • Data model — unchanged, as intended. Stored conditions keep the
    <ref>.entity.<field> format; no migration.
  • The walk was described as "cycle-guarded" and as handling "multi-target
    reference fields" — neither was accurate. There is no visited-set (traversal is
    bounded by finite path length and one-level-per-interaction expansion), and
    neither the old walk nor typed_data handles per-item target types (e.g.
    dynamic_entity_reference). Those, plus back-references and custom-storage
    filtering, are noted as out of scope for separate issues.

Testing: phpcs, phpstan, and the
full PHPUnit suite are green locally (Unit + Kernel + Functional). The MR pipeline
is green; its three advisory (allow_failure) jobs are handled — cspell
and phpmd cleaned up, and phpunit (next major) fails identically on
1.0.x today, so it's a pre-existing moving-target job, not from this change.

Retitling this to "Use core Typed Data for FieldValue reference traversal (via
a property_traversal submodule)", since the old title names a module we ended up
not using.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.