Message Chains
debt(d5/e3/b5/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpmd and phpstan as the tools, and automated is marked 'no' — meaning these tools can flag the pattern but it requires deliberate configuration of a specialist static analysis tool rather than a default linter or compiler check. Not silent in production (d7/d9) because the pattern is structurally visible in code review, but it won't be caught by a basic linter pass.
Closest to 'simple parameterised fix' (e3). The quick_fix describes adding a single delegation method (e.g. $invoice->getCustomerCity()) to replace the chain. This is a small, localised refactor — add one method on the outer object, update callers of the chain — contained within one or a few files. Not a one-line swap (e1) because callers must be updated, but not a multi-component refactor either.
Closest to 'persistent productivity tax' (b5). The applies_to scope covers web, cli, and queue-worker contexts broadly. The why_it_matters states that the caller must understand every intermediate object's API, meaning the coupling tax is paid by every developer who touches the calling code. However, each individual chain is localised — it doesn't define the whole system's shape (b7/b9) — so b5 fits: it slows down multiple work streams without being fully architectural.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field explicitly states that developers believe message chains are 'readable and expressive so they should be encouraged,' directly contradicting the actual problem: each navigation step couples the caller to an intermediate type. This is a well-documented gotcha (Law of Demeter) but it contradicts the intuition that fluent-looking code is good code. The additional common_mistake of confusing message chains with builder patterns (which also chain) reinforces that the trap is serious — developers actively conflate two superficially similar but fundamentally different patterns.
Also Known As
TL;DR
Explanation
Message chains occur when code navigates a deep object graph to retrieve a value — each navigation exposes the internal structure of the intermediate objects. This violates the Law of Demeter and creates brittle code: any change to the intermediate objects breaks the chain. The remedy is Hide Delegate: introduce a method on the first object that returns what the caller actually needs, keeping the navigation internal. Short chains on a single object or fluent builder are not this smell — the issue is cross-object graph traversal.
Common Misconception
Why It Matters
Common Mistakes
- Chaining through DTOs to reach a nested value — add a convenience accessor on the outer object.
- Not recognising that each method call in a chain is a navigation step through the object graph.
- Confusing message chains with builder patterns — builders chain on the same object; message chains traverse different objects.
- Hiding message chains inside private methods — the smell moves but the coupling remains.
Code Examples
$city = $order->getCustomer()->getAddress()->getCity()->getName();
// Add getCustomerCity() to Order — hide the internal navigation
$city = $order->getCustomerCity();