Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 20 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
141 changes: 141 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -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).
Loading