Winter Boot AOP: Aspect-Oriented Programming Guide
Aspect-Oriented Programming (AOP) lets you attach behavior to method calls without changing the method itself. In Winter Boot, AOP is expressed entirely through PHP 8 attributes. When the container finds an AOP attribute on a bean method, it wraps that method in an interceptor chain. The method body only runs if every interceptor allows it, so a single attribute can enforce a security check, manage a transaction, or cache a result. All AOP contracts live in the dev\winterframework\stereotype\aop\ namespace.
Built-in AOP attributes
Section titled “Built-in AOP attributes”Winter Boot ships several ready-to-use AOP attributes. Some of them must first be switched on with an #[Enable...] attribute on your #[WinterBootApplication] class, as noted below.
#[Transactional]
Wraps the annotated method in a database transaction. The transaction commits when the method returns normally, or rolls back if an exception is thrown.
use dev\winterframework\txn\stereotype\Transactional;
#[Service]class OrderService {
#[Transactional] public function createOrder(array $items): Order { // All DB operations here run inside a single transaction }}#[Cacheable]
Caches the method’s return value. Subsequent calls with the same arguments return the cached result without executing the method body.
use dev\winterframework\cache\stereotype\Cacheable;
#[Service]class ProductCatalog {
#[Cacheable(cacheNames: "products")] public function findProduct(int $id): Product { // Expensive DB lookup — only runs on cache miss }}#[Lockable]
Acquires a distributed lock before the method body executes and releases it afterwards, preventing concurrent duplicate execution.
use dev\winterframework\stereotype\concurrent\Lockable;
#[Service]class InventoryService {
#[Lockable(name: "inventory-update")] public function adjustStock(int $productId, int $delta): void { // Only one thread/process executes this at a time }}#[Async]
Schedules the method to run asynchronously in a worker process. The caller returns immediately without waiting for the result.
use dev\winterframework\task\async\stereotype\Async;
#[Service]class EmailService {
#[Async] public function sendWelcomeEmail(string $address): void { // Runs in a background worker }}#[Scheduled]
Calls a method on a fixed schedule. Supports fixed delay, fixed rate, and an initial delay, all in seconds.
use dev\winterframework\task\scheduling\stereotype\Scheduled;
#[Service]class ReportGenerator {
#[Scheduled(fixedRate: 60, initialDelay: 5)] public function generateDailyReport(): void { // Called every 60 seconds, first run after 5 seconds }}#[Traceable]
Wraps the method in a distributed-tracing span that records its duration and any exception.
use dev\winterframework\telemetry\stereotype\Traceable;
#[Service]class CheckoutService {
#[Traceable] public function processCheckout(Cart $cart): Receipt { // A trace span is created around this call }}Custom AOP attributes
Section titled “Custom AOP attributes”You can create your own AOP attributes using two components: an attribute class that implements AopStereoType, and an interceptor class that implements WinterAspect. Once both classes live in a scanned namespace, apply the attribute to any managed bean method.
-
Create the attribute class
Declare a PHP attribute class, annotate it with
#[StereoTyped], and implement theAopStereoTypeinterface. The attribute class must implement two methods:isPerInstance(): bool— returntrueif a new interceptor instance should be created per bean instance, orfalsefor a shared stateless interceptor.getAspect(): WinterAspect— return the interceptor object (lazily constructed).
TimedLog.php use Attribute;use dev\winterframework\stereotype\StereoTyped;use dev\winterframework\stereotype\aop\AopStereoType;use dev\winterframework\stereotype\aop\WinterAspect;use dev\winterframework\reflection\ref\RefMethod;use dev\winterframework\reflection\support\StereoTypeValidations;use dev\winterframework\type\TypeAssert;#[Attribute(Attribute::TARGET_METHOD)]#[StereoTyped]class TimedLog implements AopStereoType {use StereoTypeValidations;private ?TimedLogInterceptor $interceptor = null;public function __construct(public int $thresholdMs = 500) {}public function isPerInstance(): bool {return false; // shared stateless interceptor}public function getAspect(): WinterAspect {if (!isset($this->interceptor)) {$this->interceptor = new TimedLogInterceptor();}return $this->interceptor;}public function init(object $ref): void {/** @var RefMethod $ref */TypeAssert::typeOf($ref, RefMethod::class);$this->validateAopMethod($ref, 'TimedLog');}} -
Create the interceptor class
Implement
WinterAspectwith all five lifecycle methods.AopContextcarries method reflection and the application context.AopExecutionContextcarries the target object, arguments, and execution control handles.TimedLogInterceptor.php use dev\winterframework\stereotype\aop\WinterAspect;use dev\winterframework\stereotype\aop\AopContext;use dev\winterframework\core\aop\AopExecutionContext;use dev\winterframework\util\log\Wlf4p;use Throwable;class TimedLogInterceptor implements WinterAspect {use Wlf4p;const START_TIME = 'TimedLog_start';public function begin(AopContext $ctx, AopExecutionContext $exCtx): void {// Runs BEFORE the method body — stash the start time$exCtx->setVariable(self::START_TIME, microtime(true));}public function beginFailed(AopContext $ctx,AopExecutionContext $exCtx,Throwable $ex): void {// Runs if begin() threw an exceptionself::logError('TimedLog begin failed for ' . $ctx->getMethod()->getName());}public function commit(AopContext $ctx,AopExecutionContext $exCtx,mixed $result): void {// Runs AFTER the method body succeeds — $result is the return value$durationMs = $this->durationMs($exCtx);$method = $ctx->getMethod()->getName();/** @var TimedLog $attr */$attr = $ctx->getStereoType();if ($durationMs >= $attr->thresholdMs) {self::logWarning($method . ' took ' . round($durationMs, 2) . 'ms');} else {self::logInfo($method . ' took ' . round($durationMs, 2) . 'ms');}}public function commitFailed(AopContext $ctx,AopExecutionContext $exCtx,mixed $result,Throwable $ex): void {// Runs if commit() threw an exceptionself::logError('TimedLog commit failed for ' . $ctx->getMethod()->getName());}public function failed(AopContext $ctx,AopExecutionContext $exCtx,Throwable $ex): void {// Runs if the method body threw an exception$durationMs = $this->durationMs($exCtx);self::logError($ctx->getMethod()->getName() . ' failed after '. round($durationMs, 2) . 'ms: ' . $ex->getMessage());}private function durationMs(AopExecutionContext $exCtx): float {$start = $exCtx->getVariable(self::START_TIME);if (!is_float($start)) {return 0.0;}return (microtime(true) - $start) * 1000;}} -
Apply your custom attribute
Once both classes are in a scanned namespace, apply the attribute to any managed bean method:
OrderService.php use dev\winterframework\stereotype\Service;#[Service]class OrderService {#[TimedLog]public function createOrder(array $items): Order {// Duration is logged on every call; warns when it exceeds 500msreturn $this->orderRepository->save($items);}#[TimedLog(thresholdMs: 200)]public function exportAllOrders(): array {// Warns when the export takes 200ms or longerreturn $this->orderRepository->findAll();}}
AOP on controllers
Section titled “AOP on controllers”In a #[RestController], put AOP attributes only on endpoint methods — the methods marked with #[GetMapping], #[PostMapping], or the other mapping attributes. Those methods run when someone calls their URL, and your advice runs with them.
Do not put AOP attributes on plain helper methods inside a controller. Helpers run as ordinary method calls, so the advice never runs for them; Winter Boot logs a warning at startup naming each such method. If a helper needs guarding, move that logic into a #[Service] bean, where AOP works on every public method.
On an endpoint, the aspect receives the method’s arguments in declaration order, the same as on a service. A #{...} template such as #[Lockable(name: "order-#{id}")] resolves against the #[PathVariable] and #[RequestParam] values by parameter name.
WinterAspect lifecycle reference
Section titled “WinterAspect lifecycle reference”The five methods of WinterAspect form the interceptor pipeline around every method call. Each method fires at a distinct point in the execution flow.
| Method | When it runs | Key use cases |
|---|---|---|
begin(ctx, exCtx) |
Before the method body | Authentication, acquiring locks, starting transactions |
beginFailed(ctx, exCtx, ex) |
When begin() throws |
Logging, cleanup after a failed pre-check |
commit(ctx, exCtx, result) |
After the method body succeeds | Caching the result, committing transactions |
commitFailed(ctx, exCtx, result, ex) |
When commit() throws |
Rolling back a partial commit |
failed(ctx, exCtx, ex) |
When the method body throws | Rolling back transactions, releasing locks, logging errors |
Commit failures
Section titled “Commit failures”By default, an exception thrown from an aspect’s commit() is passed to commitFailed(), logged, and swallowed — the caller still receives the method’s result. That is the right behavior for side concerns such as caching or tracing.
When a commit failure means the work was not saved (a transaction that could not commit, for example), implement the PropagatesCommitFailure marker interface on your aspect. Every aspect’s commit()/commitFailed() still runs, and then the first failure from a propagating aspect is rethrown to the caller. The built-in transaction aspect works this way.
use dev\winterframework\stereotype\aop\PropagatesCommitFailure;use dev\winterframework\stereotype\aop\WinterAspect;
class OutboxAspect implements WinterAspect, PropagatesCommitFailure { // begin(), commit(), ... as usual}AopExecutionContext — execution control
Section titled “AopExecutionContext — execution control”AopExecutionContext gives an interceptor full control over whether the method body runs and what value the caller receives.
// Stop the method from executing; supply the value the caller will receive$exCtx->stopExecution(null);
// Retrieve the arguments the original method was called with$args = $exCtx->getArguments();
// Retrieve the bean instance the method belongs to$bean = $exCtx->getObject();
// Store and retrieve custom variables shared across the pipeline phases$exCtx->setVariable('txId', $transactionId);$txId = $exCtx->getVariable('txId');AopContext — method reflection
Section titled “AopContext — method reflection”AopContext carries the reflection metadata for the intercepted method call, giving you access to the method name, the attribute instance, and the full application context.
// Full reflection object for the intercepted method$method = $ctx->getMethod(); // RefMethod$name = $ctx->getMethod()->getName(); // e.g. "exportAllOrders"
// The AOP attribute instance (your custom attribute class)$attr = $ctx->getStereoType();
// The application context — access any bean from within the interceptor$appCtx = $ctx->getApplicationContext();$repo = $appCtx->beanByClass(UserRepository::class);Next steps
Section titled “Next steps”See the AOP guard example for a complete runnable app: a custom #[RequireCustomHeader] attribute protecting a REST endpoint with a 403 deny path.