mindtwo/laravel-decorator
A lightweight decorator pattern implementation for Laravel. Wrap objects and services with reusable decorators to add behavior without modifying the original class, keep concerns separated, and resolve decorated instances cleanly through the container.
Install
composer require mindtwo/laravel-decorator
The service provider is auto-discovered (Laravel package discovery); there is nothing to register for a default setup. If you keep a providers array in config/app.php for a legacy app, add Mindtwo\LaravelDecorator\LaravelDecoratorServiceProvider manually.
Where to look first
README.md in the package — the attribute-based example is the canonical usage and is the fastest path to a working decorator.src/Attributes/ — the decoration attribute(s) you place on the class you want to wrap.src/ — the base decorator class/trait that provides the passthrough behaviour and access to the wrapped instance.php artisan vendor:publish and pick the package's provider/tag from the printed list rather than guessing the tag name.First use case
You have a service that is already injected in ten places and you want to add caching/logging/metrics without editing it or touching every call site. Decorate it:
namespace App\Services;
use Mindtwo\LaravelDecorator\Attributes\Decorate;
#[Decorate(ReportServiceDecorator::class)]
class ReportService
{
public function daily(): array { /* expensive */ }
}
namespace App\Services;
use Mindtwo\LaravelDecorator\Decorator; // base class/trait shipped by the package
class ReportServiceDecorator extends Decorator
{
public function __construct(protected ReportService $decorated) {}
public function daily(): array
{
return cache()->remember('reports.daily', 300, fn () => $this->decorated->daily());
}
}
Then resolve normally — app(ReportService::class) or constructor injection returns the decorator. This is the whole value proposition: container-level wrapping with zero changes to consumers.
Keep the decorator a substitute, not a subclass of convenience. The decorator must satisfy the same contract consumers type-hint against (interface, or the concrete class where the package permits it). If ReportService has a ReportServiceInterface, bind the interface in a service provider and let the decorator implement the same interface — this keeps static analysis honest and makes the wrapper swappable.
Constructor-inject the inner instance explicitly. Do not call app(ReportService::class) from inside the decorator's constructor or methods; the container will hand you the decorator again and you get a circular dependency or infinite recursion. Declare the decorated type as a constructor argument and let the package wire it.
Order matters when stacking. Multiple decorators applied to one class compose in declaration/config order. Decide deliberately: caching should sit outside logging if you don't want cache hits logged, and inside metrics if you want to measure origin latency. Write the ordering down in a comment next to the attribute — it is the thing future maintainers get wrong.
Environment-scoped decoration. Register decorators conditionally in a service provider (if (app()->isProduction())) for things like profiling or tracing wrappers, so local stack traces and Xdebug stay usable. The attribute is static, so environment gating belongs in the provider/config layer, not in the class.
Prefer decorating at the boundary. Decorators are a good fit for repository/service classes resolved from the container: caching, retry, rate-limiting, audit logging, feature-flagged implementations. They are a poor fit for Eloquent models, controllers, and anything that is frequently new'd or serialized — the container never sees those instances.
Testing. Because the decorator is just a class, unit-test it by passing a mock of the inner instance to its constructor. For integration assertions, resolve the type from the container and assert assertInstanceOf(SomeDecorator::class, app(SomeService::class)) in a feature test — this catches a silently broken registration (wrong namespace, cached config, attribute typo) that unit tests never will.
Registering explicitly when attributes are impractical. For third-party or vendor classes you cannot annotate, use the package's config/provider registration path instead and map the abstraction to the decorator. Keep all such mappings in one provider method so the container graph is discoverable in a single file.
new ReportService() bypasses decoration entirely — as does anything the framework instantiates itself (controllers, middleware, form requests). If your decorator "doesn't run", check how the object is being created before suspecting the package.php artisan config:clear and php artisan optimize:clear after editing the config or provider. In production with config:cache/optimize in the deploy pipeline, a stale config cache will quietly keep the old mapping — re-run php artisan optimize as part of deploy.serialize() will carry the decorator wrapper; keep decorators stateless (no request-scoped data in properties) or you will get surprises on unserialize.dd(get_class(app(SomeService::class))) or app()->make() in tinker is the quickest confirmation of which implementation is live. If it returns the raw class, the registration never happened.composer.lock was removed and Laravel/PHP compatibility constraints were refreshed. No API changes, no deprecations, so upgrading from 3.0.3 is a routine composer update. Two things to check when you bump: (a) your composer.json Laravel constraint now has to intersect with the package's refreshed constraint, and (b) because the package no longer ships a lock file it resolves against your dependency graph — if you were implicitly relying on the versions the package had pinned, run your test suite before deploying.How can I help you explore Laravel packages today?