From 88756158985e4d5e98a7856033ebf9f4574695f4 Mon Sep 17 00:00:00 2001 From: WalterWoshid Date: Sat, 5 Sep 2026 20:59:37 +0200 Subject: [PATCH] Document AOP architecture with Mermaid diagrams --- README.md | 44 ++++++-------- docs/architecture.md | 141 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 24 deletions(-) create mode 100644 docs/architecture.md diff --git a/README.md b/README.md index dff2bed..dea8fed 100644 --- a/README.md +++ b/README.md @@ -755,33 +755,29 @@ $firstLog = $logs[0]; ## General workflow when a class is loaded -- The `AutoloadInterceptor` service intercepts the loading of a class - -- The `AspectMatcher` matches the class and method names with the list of - aspects and their configuration - -- If the class and method names match an aspect, query the cache state to see - if the source code is already cached - - - Check if the cache is valid: - - Modification time of the caching process is less than the modification - time of the source file or the aspect file - - Check if the cache file, the source file and the aspect file exist - - - If the cache is valid, load the proxied class from the cache - - If not, return a stream filter path to the `AutoloadInterceptor` service +PHP-AOP builds on CodeTransformer's autoloader, stream filters, and cache. After +kernel initialization, eligible classes follow this simplified flow: + +```mermaid +flowchart TD + Request["Class requested"] --> Cache{"Reusable cache state?"} + Cache -->|Yes| Cached["Load the source recorded by the cache state"] + Cache -->|No| Match["Match aspects and transformers"] + Match --> Any{"Anything matched?"} + Any -->|No| Original["Load original source"] + Any -->|Yes| Transform["Apply transformers, then weave matched advice"] + Transform --> Load["Update cache state and load resulting source"] +``` -- The `StreamFilter` modifies the source code by applying the aspects - - Convert the original source code to a proxied class - (MyClass -> MyClass__AopProxied) - - The proxied class should have the same amount of lines as the original - class (because the debugger will point to the original class) - - The proxied class extends a woven class which contains the logic of applying - the aspects - - The woven class will be included at the bottom of the proxied class - - The woven class will also be cached +Internal classes and excluded paths bypass this flow. Debug mode disables cache +reuse; development checks freshness, while production trusts existing cache state. +For weaving, the original implementation is renamed to `MyClass__AopProxied`, and +the generated `MyClass` **extends that renamed implementation**. Its intercepted +methods delegate to the advice machinery; callers keep using the original name. +See [How PHP-AOP works](docs/architecture.md) for the initialization, class-loading, +and method-invocation diagrams, with links to the implementation. ## Testing diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..2de4cd7 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,141 @@ +# How PHP-AOP works + +PHP-AOP extends CodeTransformer's class-loading and source-transformation pipeline. +CodeTransformer supplies the kernel lifecycle, dependency injection, transformer +management, stream filters, and cache infrastructure. PHP-AOP adds aspect matching, +class weaving, and advice execution. + +These diagrams describe the current implementation. They use Mermaid, which GitHub +renders directly inside Markdown. This page separates initialization, class loading, +and method execution because they happen at different times. + +## 1. Initialize the kernel + +Call your kernel's `init()` before loading the classes you want to intercept. + +```mermaid +flowchart TD + Start["Application calls MyKernel::init()"] --> Exists{"Kernel instance exists?"} + Exists -->|Yes| Kernel["Use the kernel instance"] + Exists -->|No| DI["CodeTransformer: register dependency injection"] + DI --> Decorate["AOP: provide specialized loader, processor, managers and cache services"] + Decorate --> Create["Create the kernel through dependency injection"] + Create --> Kernel + Kernel --> Initialized{"Already initialized?"} + Initialized -->|Yes| Repeat["Return, or throw if double initialization is forbidden"] + Initialized -->|No| Configure["Collect aspects, transformers and options; run configureOptions()"] + Configure --> Aspects["AOP: instantiate aspects and register their advice"] + Aspects --> Services["CodeTransformer: register options, transformers and cache state"] + Services --> Filters["CodeTransformer: register transformation and cached-source filters"] + Filters --> Loader["Install the AOP-aware Composer class loader"] + Loader --> Ready["Mark the kernel initialized"] +``` + +The specialization uses service decoration: AOP's class loader extends +CodeTransformer's loader, and `AspectProcessor` extends `TransformerProcessor`. +The aspect and transformer managers can use the application's custom dependency +injection callback to construct components. + +Implementation: [AopKernel](../src/AopKernel.php), plus the inherited +`CodeTransformerKernel` lifecycle from the `okapi/code-transformer` dependency. + +## 2. Load a class + +The AOP loader first asks Composer for the source file. Internal framework classes +and excluded paths bypass matching and transformation. + +```mermaid +flowchart TD + Request["PHP requests a class"] --> Locate["Composer locates its source file"] + Locate --> Found{"File found?"} + Found -->|No| Missing["Return false to the autoload chain"] + Found -->|Yes| Bypass{"Internal class or excluded path?"} + Bypass -->|Yes| Original["Load original source"] + Bypass -->|No| Cache{"Cache state reusable?"} + Cache -->|Yes| CachedFile{"Cached source path available?"} + CachedFile -->|No| Original + CachedFile -->|Yes| Cached["Load cached source through the cached-source filter"] + Cache -->|No| Match["Match AOP advice and CodeTransformer transformers"] + Match --> Any{"Anything matched?"} + Any -->|No| Original + Any -->|Yes| Filter["Load through CodeTransformer's transformation stream filter"] + Filter --> Transform["Run matched transformers first"] + Transform --> Advice{"Advice matched and class eligible for weaving?"} + Advice -->|Yes| Weave["AOP: rename implementation and generate woven class"] + Advice -->|No| Save + Weave --> Save["Save changed source and any woven file; update cache state"] + Save --> Load["PHP loads the resulting source"] +``` + +Cache reuse depends on the configured mode: + +| Mode | Behavior | +| --- | --- | +| Debug enabled | Bypass cache reuse and match again. | +| Development | Reuse existing cache state only when `isFresh()` succeeds. | +| Production | Trust existing cache state without checking source freshness. | + +For a reusable state, the loader either returns the original source path or loads +the recorded cache path through a stream filter. When rebuilding, processing can +record woven, transformed-only, or no-transformation cache state. A class with no +matching advice or transformers returns its original path immediately. + +When a class is woven, the generated relationship is: + +```php +// Renamed implementation, retaining its original parent if it had one: +class MyClass__AopProxied extends OriginalParent { /* Original implementation */ } + +// Generated class under the name application code already uses: +class MyClass extends MyClass__AopProxied { /* Intercepted method wrappers */ } +``` + +For a class without an original parent, the first declaration has no `extends` +clause. The woven class extends the renamed implementation. Application code keeps +using `MyClass`; it does not switch to a separate wrapper object. Source rewriting +and the stream filters preserve source-file locations for debugging. + +Implementation: [ClassLoader](../src/Core/AutoloadInterceptor/ClassLoader.php), +[AspectProcessor](../src/Core/Processor/AspectProcessor.php), +[ProxiedClassModifier](../src/Core/Transform/ProxiedClassModifier.php), and +[WovenClassBuilder](../src/Core/Transform/WovenClassBuilder.php). + +## 3. Invoke an intercepted method + +Class loading prepares the wrappers. Each subsequent call to an intercepted method +uses the registered advice; it does not repeat source matching and weaving. + +```mermaid +flowchart TD + Call["Application calls an intercepted method"] --> Wrapper["Generated method delegates to Interceptor"] + Wrapper --> Before["Run Before advice; carry forward argument changes"] + Before --> HasAround{"Around advice registered?"} + HasAround -->|No| Direct["Invoke the renamed implementation's method"] + HasAround -->|Yes| Around + subgraph Chain["Inside the Around advice chain"] + Around["Enter Around advice"] --> Needed{"Chain reaches original without an established result?"} + Needed -->|Yes| Original["Invoke original inside the chain, possibly through proceed()"] + Original --> Resume["Resume Around advice and resolve the final chain result"] + Needed -->|No| Resume + end + Direct --> After["Run After advice with the current result"] + Resume --> After + After --> Result["Return the final result to the caller"] +``` + +This is the normal successful path. An exception interrupts it; `After` is not a +`finally` handler. Missing advice groups are skipped. The configured advice order +is applied within the Before, Around, and After groups. + +An Around invocation's `proceed()` advances the chain. The original method can run +inside that call; the Around advice then resumes and can modify the result. The +chain also continues +after an advice returns; merely omitting `proceed()` is not sufficient to suppress +the original call. Supplying a result can avoid the original call. The chain +normally reuses an established result; `proceed(true)` permits repeated original +calls, including from After advice. An explicit `setResult(null)` distinguishes a +replacement null result from an advice that returns no replacement value. + +Implementation: [Interceptor](../src/Core/Intercept/Interceptor.php), +[AdviceChain](../src/Core/Invocation/AdviceChain.php), and +[MethodInvocation](../src/Invocation/MethodInvocation.php).