One of the most expensive kinds of technical debt is the extension that works perfectly until the next framework upgrade.
I have been burned by this enough times to stop treating it as a minor inconvenience. The pattern is familiar: you need to change a framework behavior, inheritance seems like the fastest option, the application works, a major version changes something underneath, and your extension becomes fragile or broken. That is not just annoying. It is a sign that the extension point was weak from the start.
The work I started doing around layout-core, and the thinking that also led to aspect-me, comes from that frustration. I wanted a better answer to one question: how do I change framework behavior without coupling so tightly to internals that upgrades become a fight every time?
The upgrade problem
Most framework customizations start with good intentions.
You need slightly different behavior, so you subclass something, override a method, or copy an internal class and tweak it. Short term, this feels productive. The application gets exactly what it needs.
The problem is that deep inheritance is too intimate for long-lived extensions. Once you override a framework class deeply enough, you are not just depending on the public contract. You are depending on private design choices — the order methods are called, the shape of internal state, assumptions between methods that the framework authors never intended to be part of any external interface. That is why upgrades hurt. The framework team changes something entirely reasonable from their perspective, and your customization turns out to have been standing on exactly that internal behavior.
I stopped seeing upgrade breakage as bad luck. Most of the time it is the architecture telling the truth: the extension was never as clean as it looked.
This is especially acute in Laravel because the ecosystem moves quickly. Major versions bring real improvements, and falling behind has its own cost. Every customization that makes upgrading painful also makes staying current harder, which means the application drifts further from the framework’s maintained path.
What Magento got right
Magento influenced this part of my thinking considerably.
Whatever else people say about Magento, it took extension seriously. Observers, plugins, interceptors, preferences, and configuration-driven behavior are all attempts to create change points without forcing every customization into brittle inheritance. You can change product save behavior without touching the product save class. You can add behavior before and after a method call without subclassing the class that contains it.
Those mechanisms are not all equally elegant, but they reflect an important architectural idea: extension points should be part of the framework design, not an accidental side effect of class visibility. When a Magento module works well, it works because it is using a designed extension seam rather than overriding an implementation detail.
Laravel is cleaner in many ways, but it does not always prioritize extension in that same explicit way. The first instinct is often to subclass, override, or bind something custom into the container. Sometimes that is fine. More often than I used to acknowledge, it is not upgrade-safe enough.
Why inheritance keeps failing at the boundaries
Inheritance fails as an extension strategy when the behavior you want to change is cross-cutting, version-sensitive, or only partly different from the original.
Cross-cutting means the change needs to happen in multiple places that do not share an obvious parent class. If I need slightly different behavior in three different parts of the framework’s rendering pipeline, subclassing one does not help with the other two. I end up with multiple overrides, each depending on internal details, each needing individual attention at upgrade time.
Version-sensitive means the internal methods I am depending on change between major versions. This is guaranteed to happen eventually. The more specific my override, the more tightly coupled it is to the current implementation’s shape.
Only partly different means the override has to copy internal code to get access to private state it needs. At that point the customization is not extending the framework, it is duplicating it — with all the maintenance consequences of having two copies of the same logic.
For layout and rendering behavior specifically, these problems compound. Layout systems have a lot of internal structure, and a small change to rendering output can depend on several internal methods that were never designed for external extension. Subclassing into the middle of that creates exactly the kind of fragility that makes upgrades painful.
Decorators and interception as better patterns
What started making more sense was more explicit indirection.
A decorator wraps an implementation and delegates to it, intercepting before or after the call. It is a standard pattern, but in the context of framework extension, it has a significant advantage. The decorator depends on an interface or a public method signature, not on internal state or call order. When the underlying implementation changes in a major version, the decorator only needs to change if the public contract changes — a much smaller and more predictable surface.
Proxy-style interception takes this further. If I can intercept method calls without subclassing the target, I can add behavior before or after a method without seeing its internal structure at all. The target class can change its implementation completely as long as the method signature stays stable.
That is the idea behind aspect-me. If method interception can be expressed as an explicit, declared thing rather than a subclass with an overridden method, customization becomes less tightly coupled to the internals of the thing being customized. The change is still there, but expressed at the right level of abstraction.
With layout-core, the same principle applies to layout behavior specifically. Instead of asking application developers to subclass the framework’s layout internals, the goal is to expose a stable abstraction around layout behavior that extensions can depend on. If the underlying implementation changes, the abstraction can absorb it. The extension only needs to change if the abstraction itself changes, which happens far less often than implementation details do.
The principle behind upgrade safety
The principle: if the extension point is part of the abstraction, the underlying implementation has more room to change.
Upgrade safety is not mostly a testing problem. It is a design problem. If my extension depends on framework internals, careful testing will find the breakage, but it will not prevent it. The breakage was built in at design time. The right answer is to build extension points that depend on stable contracts rather than on implementation details, so that the next major version does not require touching customization code that was working correctly.
This also means being honest about what is and is not a stable contract in a framework. Just because a class is public and visible does not mean it is designed for external extension. The public API of a framework is smaller than its visible surface. Learning to identify the difference between “this is a designed extension point” and “this is an implementation detail that happens to be accessible” is one of the more useful skills in framework-heavy development.
Where this work is going
layout-core and aspect-me are both in early stages. What they represent is a direction: extension patterns that are more explicit about their seams, that depend on stable contracts rather than on internal details, and that make upgrade-time surprises less likely.
But once you have carried a system through enough major version upgrades — fixing broken subclasses, hunting for renamed methods, reconstructing overrides after internal restructuring — that cost becomes impossible to ignore. The time to make better choices is before the upgrade, not after.
The work on mods-framework earlier this year and now these extension-point experiments are part of the same thread: understanding what it means to build Laravel applications that stay maintainable and extensible over time, rather than just functional at the moment they are delivered.
That is the problem I find most interesting right now in PHP application architecture.