AOP Sample Application
Build an app that guards a REST endpoint with a custom AOP attribute. You annotate the endpoint method with #[RequireCustomHeader] and the framework runs your interceptor before the method body: requests carrying X-Custom-Foo-Bar: foo-bar go through, everything else is denied with 403 without the method executing. It applies the concepts from AOP — attributes, interceptors, and the advice lifecycle — in a complete runnable app.
Prerequisites
Section titled “Prerequisites”You need PHP 8.5 or later with the winter_boot, swoole, and pcntl extensions (see the Installation guide). No external services are needed.
Project structure
Section titled “Project structure”The sample uses this layout:
aop-guard/├── bin/│ └── application.php # Application entry point├── config/│ └── application.yml # Server and app identity├── src/│ ├── AopSampleApplication.php # Main application class│ ├── aop/│ │ ├── RequireCustomHeader.php # The AOP attribute│ │ └── RequireCustomHeaderInterceptor.php # The advice│ └── rest/│ └── AopDemoController.php # Guarded endpoint└── composer.json # DependenciesInstall dependencies
Section titled “Install dependencies”Install the framework package as described in the Installation guide (Docker is recommended).
Source files
Section titled “Source files”Switch between the source files. Each tab shows the exact file from the sample.
The single entry point. No extra #[Enable*] attribute is needed — AOP support is always on.
<?php
namespace dev\example;
use dev\winterframework\stereotype\WinterBootApplication;
#[WinterBootApplication( configDirectory: [__DIR__ . "/../config"], scanNamespaces: [ ['dev\\example', __DIR__ . ''] ])]class AopSampleApplication {
public static function main(): void { $winterApp = new \dev\winterframework\core\app\WinterWebSwooleApplication(); $winterApp->run(self::class); }}The attribute. It declares which header and value to require (with defaults), hands out one shared interceptor instance, and validates at boot that it sits on a suitable public method.
<?phpdeclare(strict_types=1);
namespace dev\example\aop;
use Attribute;use dev\winterframework\reflection\ref\RefMethod;use dev\winterframework\reflection\support\StereoTypeValidations;use dev\winterframework\stereotype\StereoTyped;use dev\winterframework\stereotype\aop\AopStereoType;use dev\winterframework\stereotype\aop\WinterAspect;use dev\winterframework\type\TypeAssert;
#[Attribute(Attribute::TARGET_METHOD)]#[StereoTyped]class RequireCustomHeader implements AopStereoType { use StereoTypeValidations;
private ?RequireCustomHeaderInterceptor $interceptor = null;
public function __construct( public string $headerName = 'X-Custom-Foo-Bar', public string $expectedValue = 'foo-bar' ) { }
public function isPerInstance(): bool { return false; // shared stateless interceptor }
public function getAspect(): WinterAspect { if (!isset($this->interceptor)) { $this->interceptor = new RequireCustomHeaderInterceptor(); } return $this->interceptor; }
public function init(object $ref): void { /** @var RefMethod $ref */ TypeAssert::typeOf($ref, RefMethod::class); $this->validateAopMethod($ref, 'RequireCustomHeader'); }}#[StereoTyped] is what makes the scanner discover your attribute class. isPerInstance() => false shares one interceptor across all guarded methods.
The advice. begin() runs before the endpoint body: it reads the header from the request and calls stopExecution() with a 403 response on mismatch, so the body never runs. Throwing from begin() would surface as a 500 — denying via stopExecution() is what produces the 403.
<?phpdeclare(strict_types=1);
namespace dev\example\aop;
use dev\winterframework\core\aop\AopExecutionContext;use dev\winterframework\exception\WinterException;use dev\winterframework\stereotype\aop\AopContext;use dev\winterframework\stereotype\aop\WinterAspect;use dev\winterframework\util\log\Wlf4p;use dev\winterframework\web\http\HttpRequest;use dev\winterframework\web\http\HttpStatus;use dev\winterframework\web\http\ResponseEntity;use Throwable;
class RequireCustomHeaderInterceptor implements WinterAspect { use Wlf4p;
public function begin(AopContext $ctx, AopExecutionContext $exCtx): void { /** @var RequireCustomHeader $stereo */ $stereo = $ctx->getStereoType();
$request = $ctx->getApplicationContext()->getCurrentHttpRequest(); if (!isset($request)) { throw new WinterException( '#[RequireCustomHeader] can only guard HTTP endpoint methods: ' . $ctx->getMethod()->getName() ); }
if ($request->getFirstHeader($stereo->headerName) !== $stereo->expectedValue) { self::logWarning('Forbidden call to ' . $ctx->getMethod()->getName()); $exCtx->stopExecution( ResponseEntity::status(HttpStatus::$FORBIDDEN)->withJson([ 'success' => false, 'data' => null, 'error' => 'Forbidden' ]) ); } }
public function beginFailed(AopContext $ctx, AopExecutionContext $exCtx, Throwable $ex): void { self::logError('RequireCustomHeader begin failed: ' . $ex->getMessage()); }
public function commit(AopContext $ctx, AopExecutionContext $exCtx, mixed $result): void { self::logInfo('RequireCustomHeader check passed for ' . $ctx->getMethod()->getName()); }
public function commitFailed(AopContext $ctx, AopExecutionContext $exCtx, mixed $result, Throwable $ex): void { self::logError('RequireCustomHeader commit failed: ' . $ex->getMessage()); }
public function failed(AopContext $ctx, AopExecutionContext $exCtx, Throwable $ex): void { self::logError($ctx->getMethod()->getName() . ' failed: ' . $ex->getMessage()); }}The guarded endpoint, protected by the #[RequireCustomHeader] advice.
<?phpdeclare(strict_types=1);
namespace dev\example\rest;
use dev\example\aop\RequireCustomHeader;use dev\winterframework\stereotype\RestController;use dev\winterframework\stereotype\web\GetMapping;use dev\winterframework\stereotype\web\RequestMapping;
#[RestController]#[RequestMapping(path: "aop-demo")]class AopDemoController {
#[GetMapping(path: "secure-greeting")] #[RequireCustomHeader] public function secureGreeting(): array { return [ 'success' => true, 'data' => 'Hello from the AOP-guarded endpoint!' ]; }}To guard another endpoint, annotate it the same way. The header name and value are optional parameters: #[RequireCustomHeader(headerName: "X-Api-Key", expectedValue: "secret")].
The launch script. It loads the Composer autoloader and starts the application.
<?php
use dev\example\AopSampleApplication;
require_once(dirname(__DIR__) . '/vendor/autoload.php');
AopSampleApplication::main();The sample declares the framework dependency with PSR-4 autoloading for its own namespace.
{ "name": "suvera/winter-boot-aop-sample", "require": { "ext-pcntl": "*", "ext-swoole": "*", "suvera/winter-boot": "@dev" }, "autoload": { "psr-4": { "dev\\example\\": "src/" } }}Configuration
Section titled “Configuration”AOP needs no module config. The full sample application.yml sets the server and app identity:
server: port: 8080 address: 0.0.0.0 context-path: /winter: application: name: AOP Guard Sample Application id: aop-guard-sample-app version: 1.0.0See Configuration for every application.yml key.
Run the app
Section titled “Run the app”Start the application, then call the endpoint with and without the header.
1. Start the application:
composer installphp bin/application.php2. Call without the header — expect 403:
curl -i http://127.0.0.1:8080/aop-demo/secure-greetingHTTP/1.1 403 Forbidden{"success": false, "data": null, "error": "Forbidden"}The interceptor stopped execution, so the endpoint body never ran.
3. Call with a wrong value — expect 403:
curl -i -H "X-Custom-Foo-Bar: wrong" http://127.0.0.1:8080/aop-demo/secure-greeting4. Call with the right value — expect 200:
curl -i -H "X-Custom-Foo-Bar: foo-bar" http://127.0.0.1:8080/aop-demo/secure-greetingHTTP/1.1 200 OK{"success": true, "data": "Hello from the AOP-guarded endpoint!"}Next steps
Section titled “Next steps”- Read AOP for the advice lifecycle (
begin/commit/failed),stopExecution, and execution variables. - Put reusable advice on
#[Service]bean methods the same way — the attribute works there unchanged.