mykemeynell / laravel-decorators
Attribute-based method decorators (Log, Cache, Retry, …) wired into the Laravel IoC container.
Requires
- php: >=8.2
- illuminate/cache: ^11.0|^12.0|^13.0
- illuminate/container: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/log: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- code-lts/doctum: ^5.6
- larastan/larastan: ^3.0
- laravel/pint: ^1.29
- orchestra/testbench: ^11.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11
README
Python/TypeScript-style method decorators for Laravel services, powered by PHP 8 attributes.
This package provides a clean, attribute-based way to apply cross-cutting concerns (logging, caching, retries, etc.) to your Laravel service methods. It uses a lightweight proxy pattern to intercept method calls and wrap them in a decorator chain.
Requirements
- PHP:
>=8.2 - Laravel:
^11.0,^12.0, or^13.0
Installation
composer require mykemeynell/laravel-decorators
Quick Start
1. Add Attributes to Your Service
namespace App\Services; use MykeMeynell\Laravel\Decorators\Decorators\Log; use MykeMeynell\Laravel\Decorators\Decorators\Cache; class UserService { #[Log] #[Cache(ttl: 3600)] public function findUser(int $id): array { return User::findOrFail($id)->toArray(); } }
2. Resolve the Decorated Service
You can resolve your service through the Decorator facade to ensure it is wrapped in the proxy:
use MykeMeynell\Laravel\Decorators\Facades\Decorator; use App\Services\UserService; $service = Decorator::make(UserService::class); $user = $service->findUser(1); // Call is logged and cached!
Built-in Decorators
#[Log]
Records method calls, arguments, and execution time to Laravel logs.
#[Log(level: 'info', logArgs: true, channel: 'stack')]
level: PSR-compatible log level (default:debug).logArgs: Whether to include raw arguments in the log (default:true).channel: The Laravel log channel to use (default: configlog_channel).
#[Cache]
Caches the method return value based on its identity and arguments.
#[Cache(ttl: 3600, store: 'redis', tags: ['users'], prefix: 'u:')]
ttl: Time-to-live in seconds. Use0to cache forever (default:3600).store: The cache store to use (default: configcache_store).tags: Array of cache tags for taggable stores (default:[]).prefix: Key prefix (default: configcache_prefix).
#[Retry]
Transparently retries failed method calls with configurable backoff.
#[Retry(times: 3, delay: 100, backoff: 2.0, catch: [ServiceException::class])]
times: Maximum attempts including the first call (default:3).delay: Base delay in milliseconds between retries (default:0).backoff: Multiplier for exponential backoff (default:1.0).catch: Array of exception classes to retry on (default:[], catches allThrowable).log: Whether to log retry attempts (default:true).
#[RateLimit]
Throttles method execution using Laravel's rate limiter.
#[RateLimit(maxAttempts: 5, decaySeconds: 60, key: 'my-bucket')]
maxAttempts: Max calls within the window (default:60).decaySeconds: Window duration in seconds (default:60).key: Optional fixed bucket identifier. By default, keys are unique to method + arguments.
#[Transactional]
Wraps the method execution in a database transaction.
#[Transactional(connection: 'mysql', attempts: 2)]
connection: Database connection name (default: default connection).attempts: Number of times to retry the transaction on deadlock (default:1).
#[Validate]
Validates method arguments using Laravel's validator before execution.
#[Validate(['id' => 'required|integer', 'email' => 'required|email'])]
rules: Array of validation rules. Can be keyed by parameter name or index.
#[Deprecated]
Emits a deprecation warning when the method is called.
#[Deprecated('Use newMethod() instead.')]
message: Custom deprecation message. EmitsE_USER_DEPRECATEDin local/testing environments.
#[DecorateWith]
Delegates decoration behavior to an arbitrary callable. This is useful for ad-hoc decoration without creating a dedicated attribute class.
#[DecorateWith(MyCustomWrapper::class)] #[DecorateWith('App\Decorators\MyDecorator::handle')] #[DecorateWith('my_global_decorator_function')] public function myMethod() { ... }
classOrFunction: A class name (must be invokable), aClass::methodstring, or a global function name.method: Optional method name if providing a class name separately.
The callable must return another callable that performs the actual wrapping:
class MyCustomWrapper { public function __invoke(callable $next): callable { return function (array $args) use ($next) { // Pre-processing $result = $next($args); // Post-processing return $result; }; } }
Configuration
Publish the configuration file:
php artisan vendor:publish --tag="decorators-config"
Auto-Decoration
You can configure certain classes to be automatically decorated when resolved from the Laravel container:
// config/decorators.php return [ 'decorate' => [ App\Contracts\PaymentProcessor::class, ], ];
Creating Custom Decorators
Implement the MethodDecorator interface:
namespace App\Decorators; use Attribute; use MykeMeynell\Laravel\Decorators\Contracts\MethodDecorator; #[Attribute(Attribute::TARGET_METHOD)] class MyCustomDecorator implements MethodDecorator { public function wrap(callable $next, array $context = []): callable { return function (array $args) use ($next) { // Logic before $result = $next($args); // Logic after return $result; }; } }
License
The MIT License (MIT). Please see License File for more information.