Traverse FieldValue references via core Typed Data in a new property_traversal submodule

What this does

Re-bases FieldValue's entity-reference traversal onto the core Typed Data API, extracted behind a public service in a new, self-contained property_traversal submodule. No behaviour change, no stored-condition-format change.

Why not typed_data (the issue's original proposal)

The issue proposed the typed_data contrib module's DataFetcher. That was evaluated against the live build and rejected on two findings:

  1. It doesn't arrive with ECA. No ECA branch (1.1.x, 2.0.x, 2.1.x, or the installed 3.1.4) requires typed_data — it would be a genuinely new dependency, not a transitive one.
  2. Its definition traversal is lossy per hop. DataFetcher drills via EntityDataDefinition::getPropertyDefinitions(), which falls back to getBaseFieldDefinitions() unless pinned to exactly one bundle — and core builds every entity-reference hop target bundle-less (EntityReferenceItem::propertyDefinitions()). Every hop would silently lose all configurable fields.

Search API's FieldsHelper solves exactly that bundle-less gap — and does so on core Typed Data interfaces, without depending on typed_data either. This MR adopts that shape. Net result: the issue's goal (stop rediscovering traversal locally; stand on the Typed Data API) is met with no new Composer dependency.

Changes

  • New property_traversal submodule — core-only, namespaced Drupal\property_traversal. Exposes property_traversal.fetcher (PropertyTraversalInterface): rootDefinition(), innerProperty(), nestedProperties() (the load-bearing per-hop bundle-field union that core's EntityDataDefinition lacks — see #2169813), resolve() (discriminated leaf/hop/error), isReference(), isDescendable(), targetType().
  • FieldValue re-basedwalk(), isReference(), isDescendable() removed; all path questions go through the service. Hop-selector options, labels, the separator, applyToQuery()/resolveToIds(), and the stored <ref>.entity.<field> format are unchanged.
  • entity_segment now hard-depends on property_traversal (the base plugin consumes its service).
  • Tests — a stand-alone submodule kernel test (proving the service resolves with entity_segment disabled), extended FieldValue guards (computed-field exclusion, compound-column stability, non-throwing raw-path label fallback, byte-identical multi-hop round-trip), and a functional test proving the hard dependency pulls the submodule in.

⚠️ API change (updates the issue's "API changes" section)

The issue states "API changes: internal to FieldValue". That is no longer accurate: this introduces a new public service and a new submodule boundary. SegmentPluginInterface and QueryableSegmentPluginInterface are still untouched. The property_traversal machine name is deliberately unprefixed and the dependency runs one way (entity_segment → property_traversal) so the submodule can later move to its own contrib project.

Data model: unchanged — stored conditions keep the <ref>.entity.<field> format; no migration.

Testing

Locally against Drupal core in the ddev environment:

  • ddev phpcs — clean
  • ddev phpstan — no errors
  • ddev phpunit — 160 tests green (Unit 32, Kernel 103, Functional 25)

Out of scope

Per-item target types (dynamic_entity_reference), back-references, and custom-storage filtering are each noted as separate follow-ups. Extracting property_traversal into its own project is a later work order; this only shapes the folder for it.

Merge request reports

Loading