A lightweight, high-performance Aspect-Oriented Programming (AOP) package for Laravel, built specifically for PHP 8.2+ Attributes.
Spray AOP enables you to apply cross-cutting concerns to your application classes using native PHP Attributes and dynamically generated proxy classes. It hooks into the Laravel Service Container to automatically resolve proxied versions of your classes whenever aspects are detected, ensuring clean separation of logic with zero manual boilerplate.
- Features
- Requirements
- Installation
- Publish Configuration
- Configuration
- Usage
- Artisan Commands
- Production
- Notes
- License
- Modern AOP: Leverages native PHP 8.2+ Attributes for declarative interception.
- Smart Proxy Engine: Automatic proxy generation with full method signature compatibility.
- Production-Ready: High-performance proxy caching to eliminate runtime I/O overhead.
- Seamless Integration: Automatically swaps target classes within the Laravel Service Container.
- Lifecycle Hooks: Simplified logic through Spray\Aop\Aspects\BaseAspect (Around, Before, After, and Exception hooks).
- Developer Experience: Powerful Artisan commands for proxy management (cache, clear, rebuild).
- Rapid Scaffolding: Built-in generator to create Aspects and Attributes in seconds (spray:make-aspect).
- PHP 8.2 or higher
- Laravel 10.0 or higher (compatible with 11.0, 12.0, 13.0)
- Composer for dependency management
composer require sarkis-sh/spray-aopLaravel will auto-discover the provider via Spray\Aop\Providers\AopServiceProvider.
php artisan vendor:publish --tag=spray-aop-configThis publishes the configuration file to config/spray-aop.php.
The published config supports:
enabled- Enable or disable the AOP engine entirelystorage_path- Relative storage path for generated proxy classesscan_paths- Directories scanned for classes with AOP attributesauto_generate- Allow on-the-fly proxy generation when a proxy is missing
Default config values:
return [
'enabled' => env('SPRAY_AOP_ENABLED', true),
'storage_path' => 'framework/aop/proxies',
'scan_paths' => [
app_path(),
],
'auto_generate' => env('SPRAY_AOP_AUTO_GEN', true),
];You can create both using the built-in generator:
php artisan spray:make-aspect AuditLogThe generated files include:
- an Aspect handler class under
app/Aspects - a matching Attribute class under
app/Attributes
Customize your Aspect and Attribute generation with the following options:
| Option | Short | Description |
|---|---|---|
--before |
-b |
Include the before() hook in the generated Aspect |
--after |
-a |
Include the after() hook in the generated Aspect |
--around |
-x |
Include the handle() (around) method for full control over method execution |
--exception |
-e |
Include the onException() hook for exception handling logic |
--class |
-c |
Allow the Attribute to target classes |
--method |
-m |
Allow the Attribute to target methods |
--repeatable |
-r |
Make the Attribute repeatable (allows multiple instances on the same target) |
--force |
- | Force creation even if the Aspect or Attribute already exists |
Generate with before and after hooks on methods:
php artisan spray:make-aspect Logging -b -a -mGenerate with exception handling on classes:
php artisan spray:make-aspect ErrorTracker -e -cGenerate a full-featured repeatable Aspect:
php artisan spray:make-aspect PerformanceMonitor -b -a -x -e -rnamespace App\Attributes;
use Spray\Aop\Attributes\AspectAttribute;
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AuditLog extends AspectAttribute
{
public function __construct(string $action)
{
parent::__construct(\App\Aspects\AuditLogAspect::class, ['action' => $action]);
}
}namespace App\Aspects;
use Spray\Aop\Aspects\BaseAspect;
class AuditLogAspect extends BaseAspect
{
protected function before(array $invocation): void
{
logger()->info('Audit start', [
'method' => $invocation['method'],
'args' => $invocation['args'],
'options' => $invocation['options'],
]);
}
protected function after(array $invocation, mixed $result): mixed
{
logger()->info('Audit completed', ['result' => $result]);
return $result;
}
}The $invocation array contains the following keys:
'instance': The object instance being intercepted (the proxied class instance).'method': The name of the method being called (e.g.,'create').'args': An array of arguments passed to the method.'options': The configuration options defined in the attribute (e.g.,['action' => 'user.created']).
Here are complete examples of aspects generated with different options using php artisan spray:make-aspect. Each example shows the Aspect class and its corresponding Attribute.
namespace App\Aspects;
use Spray\Aop\Aspects\BaseAspect;
class LoggingAspect extends BaseAspect
{
protected function before(array $invocation): void
{
logger()->info('Method called', [
'class' => get_class($invocation['instance']),
'method' => $invocation['method'],
'args' => $invocation['args'],
]);
}
}Corresponding Attribute:
namespace App\Attributes;
use Spray\Aop\Attributes\AspectAttribute;
#[\Attribute(\Attribute::TARGET_METHOD)]
class Logging extends AspectAttribute
{
public function __construct()
{
parent::__construct(\App\Aspects\LoggingAspect::class, []);
}
}namespace App\Aspects;
use Spray\Aop\Aspects\BaseAspect;
class CachingAspect extends BaseAspect
{
protected function after(array $invocation, mixed $result): mixed
{
// Cache the result
cache()->put($invocation['method'], $result, 3600);
return $result;
}
}namespace App\Aspects;
use Spray\Aop\Aspects\BaseAspect;
use Throwable;
class ErrorTrackingAspect extends BaseAspect
{
protected function onException(array $invocation, Throwable $e): mixed
{
// Log the error
logger()->error('Exception in method', [
'method' => $invocation['method'],
'exception' => $e->getMessage(),
]);
throw $e; // Re-throw the exception
}
}For full control, use the handle method (around advice):
namespace App\Aspects;
use Spray\Aop\Aspects\BaseAspect;
use Closure;
class PerformanceAspect extends BaseAspect
{
public function handle(array $invocation, Closure $next): mixed
{
$start = microtime(true);
try {
$result = $next($invocation); // Execute the original method
$duration = microtime(true) - $start;
logger()->info('Method performance', [
'method' => $invocation['method'],
'duration' => $duration,
]);
return $result;
} catch (Throwable $e) {
$duration = microtime(true) - $start;
logger()->error('Method failed', [
'method' => $invocation['method'],
'duration' => $duration,
'error' => $e->getMessage(),
]);
throw $e;
}
}
}namespace App\Aspects;
use Spray\Aop\Aspects\BaseAspect;
use Throwable;
class AuditAspect extends BaseAspect
{
protected function before(array $invocation): void
{
logger()->info('Audit: Method starting', [
'method' => $invocation['method'],
'args' => $invocation['args'],
]);
}
protected function after(array $invocation, mixed $result): mixed
{
logger()->info('Audit: Method completed', [
'method' => $invocation['method'],
'result' => $result,
]);
return $result;
}
protected function onException(array $invocation, Throwable $e): mixed
{
logger()->error('Audit: Method failed', [
'method' => $invocation['method'],
'error' => $e->getMessage(),
]);
throw $e;
}
}use App\Attributes\AuditLog;
#[AuditLog('user.created')]
class UserService
{
public function create(array $data)
{
// business logic
}
}- Laravel resolves a service from the container
- Spray AOP inspects the class for
AspectAttributeusage - If aspects exist, it generates or loads a proxy class
- Proxied methods execute through the
Spray\Aop\Engines\Pipeline - Each aspect is executed in order, with support for
before,after, and exception handling - Proxies are generated once and cached as plain PHP files, ensuring near-native execution speed
php artisan spray:aop-cache- Pre-generate all proxy classes for productionphp artisan spray:aop-clear- Remove all generated proxy classesphp artisan spray:aop-rebuild- Clear and regenerate proxies immediatelyphp artisan spray:make-aspect- Generate a new Aspect handler and Attribute
For production deployments, disable runtime generation and cache proxies ahead of time:
SPRAY_AOP_AUTO_GEN=falseThen run:
php artisan spray:aop-cache- Spray AOP only intercepts public, non-static, non-final, non-constructor methods.
- It ignores Laravel internals and proxy classes to avoid bootstrap recursion.
- The package uses
storage/framework/aop/proxiesby default for generated PHP files. - If you change the
storage_pathin the configuration, ensure the new directory is writable by the web server.
MIT — see LICENSE