extend() lets you wrap the resolved service in a decorator without redefining the original construction logic.
This page refers to the default branch 13.x of the official Laravel documentation and to the official implementation and tests of the latest Framework release v13.40.0, checked on November 9, 2026. Besides the difference from service resolution hooks, it covers how long a decoration is retained, which changes depending on whether an instance is already stored when you register it.
Separate adding configuration from replacing the object
An
extend() callback receives the target service and the container. You can configure the same object and return it, or return a new decorator. If you forget to return, null is passed to the rest of the resolution process.
This page covers
Illuminate\Container\Container::extend(). It is a different API from the extend() on managers such as Cache and Mail, which registers a driver name and its creation logic.Build a decorator that implements the same contract
Wrap the original implementation without replacing it
For illustration, put the following contract for formatting messages insrc/MessageFormatter.php. Each of the classes below is likewise assumed to live in a PHP file of the same name in src/.
PlainMessageFormatter class, a decorator that only implements the same interface cannot stand in for it. Make MessageFormatter the dependency you expose, and keep the contract for arguments, return values, exceptions and so on. Dynamic method forwarding alone is no substitute for implementing a PHP interface.
Register the decoration in a provider
src/PrefixServiceProvider.php, which provides the decoration, does not override how the service is built. It only registers an extension with the container. For loading the provider, see Package discovery.
$formatter argument as the target service. Calling make() on the same key again inside the callback can cause circular resolution.
Bind the original implementation in the consuming application, for example in AppServiceProvider::register().
extend() before the target’s bind(). If the target has not been resolved and no instance is stored, registering alone does not build the original service. The official tests also cover lazy construction and registration before binding.
After that, app(MessageFormatter::class)->format('Shipped.') returns [Acme] Shipped.. Even if the original implementation is changed to another MessageFormatter implementation, the decorator wraps that implementation instead.
Understand where decorations apply and in what order
In a normal resolution, the container builds the original service, replaces it with each extender’s return value in registration order, stores it as a shared instance, and then calls the after-resolving hooks. If you register A and then B, the result isB(A(original service)). Consumer method calls start at the outer B, so do not confuse registration order with the order in which processing starts. If you depend on the decoration order of several packages, make the order explicit in the consuming application and test the combination.
Extenders apply to the resolution key after the container normalizes aliases. A registration on MessageFormatter::class is not applied when PlainMessageFormatter::class is resolved directly just because the types match. Standardize on using the interface.
Decorating a stored instance is retained differently
Container::extend() in v13.40.0 branches on whether an instance of the target is stored in the container.
The last row matters. If you decorate a resolved singleton and then call
forgetInstance() or rebind the same key, a decoration that was only applied on the spot is not reapplied on the next build. This differs from registering in the extender array before the first resolution.
scoped() also uses stored instances, so do not assume a decoration registered late in the middle of a scope remains registered for the next scope. Check this together with Service lifetime design.
Also, if you later store an object directly with instance(), it does not go through the loop that applies extenders during normal builds. Including test substitutions, “it is registered, so the object I pass is decorated automatically” does not always hold.
References already handed out are not replaced automatically
When you return a new wrapper, as in this example, changing the container’s stored value does not change the original object already stored in another service’s property. Arebinding() notification passes the new service to registered callbacks. It does not search for and rewrite every dependent.
Registering a notification callback with rebinding() is not always lazy either. If the target is already bound, it returns the result of make() at registration time too. On the path where you extend() a normal binding that was resolved before, make() may also be called for the notification if a rebound callback exists. Do not uniformly assume that initialization and side effects happen “on next use”.
Avoid double decoration and carried-over state
extend() does not automatically deduplicate registrations of the same closure or the same decorator. If this provider’s register() is applied twice to the same container, the prefix may be added twice as well.
- Keep decoration registration in the provider’s fixed boot path, and do not repeat it per request or job.
- Do not prevent duplicates with a single static flag shared by all containers. Tests and new applications would miss registrations they need.
- A singleton’s decoration is retained for the same shared lifetime. Do not pin the current user, request or similar state into the wrapper.
forgetExtenders()only clears the registration array. It does not turn already built wrappers back into the original.
Test registration timing with the same container
The following PHPUnit example assumes the classes above can be loaded through the package’s autoload. It applies the provider’sregister() directly to a Container and checks how long the decoration is retained without HTTP or external services. Verify auto-discovery and full application boot separately with package testing.
forgetInstance() alone as an API for removing or re-registering decorations.
Before a release, also check the order of multiple extenders, duplicate registrations, substitution with instance(), and references consumers already hold. When you add supported Laravel versions, reread the branches in extend() and the official tests, in addition to the resolution keys and type contracts.
Related pages
Service resolution hooks
Covers callAfterResolving(), which configures objects without replacing them, and what to watch for in how often it runs.
Service lifetime design
Covers bind, singleton and scoped, and the dependencies that built services hold on to.
Primary sources
- Official Laravel documentation: Extending bindings and Rebinding
- Official Laravel documentation: Service Providers
- Official Laravel documentation: Package development and discovery
- Container: extend(), resolve(), rebound(), instance(), forgetExtenders()
- Official tests: lazy construction, registration before binding, aliases, multiple extenders
- ServiceProvider: constructor and register()