Skip to main content
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. This example applies the concepts from the main AOP documentation — attributes, interceptors, and the advice lifecycle — to a complete runnable app.
Your attribute class must carry #[StereoTyped] (next to #[Attribute]). Without it the scanner never registers the attribute and your advice is silently ignored — the endpoint just runs unguarded.
In a controller, put AOP attributes only on endpoint methods — the methods marked with #[GetMapping], #[PostMapping], or the other mapping attributes. Plain helper methods inside a controller never trigger advice, even when annotated, so keep them free of AOP attributes. The same attribute works on every public method of a #[Service] bean.

Prerequisites

You need PHP 8.5 or later with the swoole and pcntl extensions. No external services are needed.

Project structure

The sample uses this layout:

Install dependencies

Require the framework package:

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.

Configuration

AOP needs no module config. The full sample application.yml sets the server and app identity:
See Configuration for every application.yml key.

Run the app

Start the application, then call the endpoint with and without the header. 1. Start the application:
2. Call without the header — expect 403:
The interceptor stopped execution, so the endpoint body never ran. 3. Call with a wrong value — expect 403:
4. Call with the right value — expect 200:

Next steps

  • Read AOP for the advice lifecycle (begin/commit/failed), stopExecution, and execution variables.
  • Put reusable advice on #[Service] beans the same way — bean-to-bean calls run through proxies, so the same attribute works there unchanged.