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:
- It doesn't arrive with ECA. No ECA branch (
1.1.x,2.0.x,2.1.x, or the installed3.1.4) requirestyped_data— it would be a genuinely new dependency, not a transitive one. - Its definition traversal is lossy per hop.
DataFetcherdrills viaEntityDataDefinition::getPropertyDefinitions(), which falls back togetBaseFieldDefinitions()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_traversalsubmodule — core-only, namespacedDrupal\property_traversal. Exposesproperty_traversal.fetcher(PropertyTraversalInterface):rootDefinition(),innerProperty(),nestedProperties()(the load-bearing per-hop bundle-field union that core'sEntityDataDefinitionlacks — see #2169813),resolve()(discriminatedleaf/hop/error),isReference(),isDescendable(),targetType(). FieldValuere-based —walk(),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_segmentnow hard-depends onproperty_traversal(the base plugin consumes its service).- Tests — a stand-alone submodule kernel test (proving the service resolves
with
entity_segmentdisabled), 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— cleanddev phpstan— no errorsddev 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.