Skip to main content
Sometimes a package needs to add behavior while keeping the service implementation the consuming application chose. The container’s 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 in src/MessageFormatter.php. Each of the classes below is likewise assumed to live in a PHP file of the same name in src/.
The undecorated implementation returns its input unchanged.
The decorator implements the same interface and delegates to the service it receives. In this example, it adds a package-specific prefix to the delegate’s return value.
If consumers type-hint the concrete 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.
The container is also passed as the callback’s second argument, but this example does not need it, so it is not declared. Use the $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().
You can also 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.
As a rule, register before the first resolution. Another provider may use the service first, so simply moving the registration to boot() does not prevent resolution before registration. Also, if you casually make a provider that only registers extensions deferred, the registration itself may lose the condition that triggers it.

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 is B(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. A rebinding() 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.
This example changes the output string. The prefix, the number of delegated calls and the exception handling also become public behavior of the package, so check compatibility instead of treating them as mere “internal implementation changes”.

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’s register() 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.
The last test does not recommend late registration. It makes the boundary in the checked version explicit. Do not design 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

Last modified on November 9, 2026