From 6c1327a3ea5cff5e8e0634c2d7d5785765a4bebd Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Fri, 8 May 2026 21:09:45 +0300 Subject: [PATCH] chore: Optimize token context consumption --- AGENTS.md | 38 +++++++- CLAUDE.md | 194 --------------------------------------- src/Aop/AGENTS.md | 52 +++++++++++ src/Core/AGENTS.md | 15 +++ src/Instrument/AGENTS.md | 64 +++++++++++++ src/Proxy/AGENTS.md | 39 ++++++++ tests/AGENTS.md | 17 ++++ 7 files changed, 224 insertions(+), 195 deletions(-) mode change 120000 => 100644 AGENTS.md delete mode 100644 CLAUDE.md create mode 100644 src/Aop/AGENTS.md create mode 100644 src/Core/AGENTS.md create mode 100644 src/Instrument/AGENTS.md create mode 100644 src/Proxy/AGENTS.md create mode 100644 tests/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 681311eb..00000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..22f67e14 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# Go! AOP Framework +goaop/framework | NS: Go\ | PHP: ^8.4.0 +AOP via source transformation at load time (stream filter, no PECL, no eval). + +## Agent gate +- PHP 8.4+ required. If PHP 8.3 or less → STOP, report can't run tests/phpstan. +- Gate: `./vendor/bin/phpstan analyze --memory-limit=512M` before commit (level 10). + +## Commands +| Action | Command | +|-----------|-----------------------------------------------------------------------| +| install | `composer install` | +| test:all | `./vendor/bin/phpunit` | +| test:file | `./vendor/bin/phpunit tests/Core/ContainerTest.php` | +| test:one | `./vendor/bin/phpunit --filter testName tests/Core/ContainerTest.php` | +| analyze | `./vendor/bin/phpstan analyze --memory-limit=512M` | + +## Architecture overview +Intercepts PHP class loading pipeline: source stream filter transforms source → injects interception hooks → caches result. +- Init: AspectKernel::init() → stream filter → transformers → configureAop() +- Main transformer: WeavingTransformer (class→trait, proxy class re-inherits parent+interfaces) +- Proxy dispatch: per-method static $__joinPoint → InterceptorInjector → advisor chain + +## Directory → AGENTS.md map +| Directory | Sub-AGENTS.md | Covers | +|-------------------|----------------------------|---------------------------------------------------------------| +| `src/Instrument/` | `src/Instrument/AGENTS.md` | Init flow, transformers, trait engine, line numbers, Override | +| `src/Proxy/` | `src/Proxy/AGENTS.md` | Proxy generators, code-gen, readonly, hooks, enums | +| `src/Aop/` | `src/Aop/AGENTS.md` | Interfaces, generics, implementations, pointcuts, attributes | +| `src/Core/` | `src/Core/AGENTS.md` | Container, aspect loading, advice matching, bridge | +| `tests/` | `tests/AGENTS.md` | Test conventions, fixtures, PHPUnit, PHPStan | + +## Rules +- Skip explanations unless asked. Show changes, not commentary. +- Use targeted edits (Edit tool) over full-file rewrites. +- No filler words ("let me", "carefully", "I'll now"). +- Before commit: phpunit and phpstan must pass. Fix errors before offering to commit. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 5c1568f4..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,194 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -**Go! AOP Framework** — an Aspect-Oriented Programming (AOP) framework for PHP 8.4+. It intercepts PHP class/method/function execution transparently by transforming source code at load time via a custom PHP stream wrapper, without requiring PECL extensions, annotations at runtime, or eval. - -Package: `goaop/framework` | Namespace root: `Go\` | PHP: `^8.4.0` - -## Agent runtime requirement - -- Agents must run on PHP **8.4 or higher**. -- If the runtime is PHP **8.3.x**, stop immediately and report that test/phpstan validation cannot be performed in that environment. - -## Commands - -```bash -# Install dependencies -composer install - -# Run full test suite -./vendor/bin/phpunit - -# Run a single test file -./vendor/bin/phpunit tests/Go/Core/ContainerTest.php - -# Run a single test method -./vendor/bin/phpunit --filter testMethodName tests/Go/Core/ContainerTest.php - -# Static analysis (PHPStan level 10, src/ only) -./vendor/bin/phpstan analyze --memory-limit=512M - -# CLI debugging tools -./bin/aspect debug:advisors [class] -./bin/aspect debug:pointcuts [expression] -``` - -## Architecture - -The framework works by intercepting PHP's class loading pipeline. When a class is loaded, the stream wrapper transforms its source code to inject interception hooks, then stores the result in a cache directory. The transformed class contains calls into the advisor chain for each matched join point. - -### Initialization flow - -1. **`AspectKernel::init()`** (`src/Core/AspectKernel.php`) — singleton, registers stream wrapper, builds transformer chain, calls `configureAop()` where users register aspects -2. **`SourceTransformingLoader::register()`** (`src/Instrument/ClassLoading/SourceTransformingLoader.php`) — PHP stream wrapper that intercepts `include`/`require` via the `go-aop-php://` protocol -3. **`AopComposerLoader::init()`** (`src/Instrument/ClassLoading/AopComposerLoader.php`) — hooks into Composer's autoloader to redirect loads through the stream wrapper -4. **`CachingTransformer`** — outer transformer that manages cache; on cache miss, invokes the inner transformers and writes the result - -### Transformer chain (inner, registered in `AspectKernel::registerTransformers()`) - -Applied in order for each loaded file: -- `ConstructorExecutionTransformer` — transforms `new` expressions (when `INTERCEPT_INITIALIZATIONS` feature enabled) -- `FilterInjectorTransformer` — wraps `include`/`require` (when `INTERCEPT_INCLUDES` enabled) -- `WeavingTransformer` — main transformer; uses `AdviceMatcher` to find applicable advices and `CachedAspectLoader` for aspect metadata, then delegates to proxy generators -- `MagicConstantTransformer` — rewrites `__FILE__`/`__DIR__` so they resolve to the original file, not the cached proxy - -> **Note:** `SelfValueTransformer` was removed in 4.0 — `self::` in traits resolves to the using class naturally. - -Each transformer returns `TransformerResultEnum`: `RESULT_TRANSFORMED`, `RESULT_ABSTAIN`, or `RESULT_ABORTED`. - -### Trait-based proxy engine (4.0) - -`WeavingTransformer` converts the original class to a **PHP trait** and writes a proxy class that uses it. The two generated files for a class `Ns\Foo` are: - -**Woven file** (replaces the original in the load stream): -```php -// Original class body converted to a trait; final/abstract/extends/implements stripped -trait Foo__AopProxied { /* original methods verbatim */ } -include_once AOP_CACHE_DIR . '/_proxies/.../Foo.php'; -``` - -**Proxy file** (loaded by the `include_once` above): -```php -class Foo extends OriginalParent implements OriginalInterfaces, \Go\Aop\Proxy -{ - use \Ns\Foo__AopProxied { - \Ns\Foo__AopProxied::interceptedMethod as private __aop__interceptedMethod; - // ... one alias per intercepted method (including private ones) - } - - public function interceptedMethod(ArgType $arg): ReturnType { - /** @var \Go\Aop\Intercept\DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = \Go\Aop\Framework\InterceptorInjector::forMethod(self::class, 'interceptedMethod', [...], $this->__aop__interceptedMethod(...)); - return $__joinPoint->__invoke($this, [$arg]); - } - // ... one override per intercepted method -} -``` - -Key properties of this engine: -- The proxy class **re-inherits** the original parent and interfaces (read from reflection, not from the woven source). -- `self::` in the trait body resolves to `Foo` (the proxy class) — no rewriting needed. -- **Private methods can be intercepted** (impossible with the old extend-based engine). -- Each generated proxy method passes a **first-class callable** as the 4th arg to `InterceptorInjector`: `$this->__aop__method(...)` for own dynamic methods, `self::__aop__method(...)` for own static methods, `parent::method(...)` for inherited methods (no trait alias), and `\functionName(...)` for function proxies. - -### Proxy generation (`src/Proxy/`) - -- `ClassProxyGenerator` — generates the trait-based proxy class for a regular class - - Takes `$traitName` (the `Foo__AopProxied` FQCN) as second constructor arg - - Always emits `use $traitName` even when no methods are intercepted (introduction-only aspects) - - Adds `__construct as private __aop____construct` alias when the class defines its own constructor and properties are intercepted -- `FunctionProxyGenerator` — generates function wrappers -- `TraitProxyGenerator` — generates trait proxies (uses old `adjustOriginalClass` path) -- `src/Proxy/Part/` — individual code-generation components: - - `InterceptedMethodGenerator` — wraps a single method with join-point delegation - - `InterceptedConstructorGenerator` — wraps constructor; uses `self::class` (not `parent::class`) for `Closure::bindTo` scope; calls `$this->__aop____construct()` when constructor is in the trait - - `InterceptedPropertyGenerator` — re-declares intercepted properties in the proxy with native PHP 8.4 `get`/`set` hooks that route through `ClassFieldAccess` -- `src/Proxy/Generator/` — low-level AST generators: - - `ClassGenerator` — builds the proxy class AST node; `addTraitAlias()` registers both the trait and an alias in a single `use { ... }` block; deduplicates traits - - `AttributeGroupsGenerator` — copies PHP 8 attributes from reflection to proxy AST, preserving named arguments - - `TypeGenerator` — converts PHP types (string or `ReflectionType`) to AST nodes or phpDoc strings; `renderTypeForPhpDoc()` is used by all proxy generators to produce the second generic argument in `@var` annotations for `$__joinPoint` - -### AOP core (`src/Aop/`) - -- `src/Aop/Intercept/` — interfaces: `Joinpoint`, `Invocation`, `MethodInvocation`, `ConstructorInvocation`, `FunctionInvocation`, `FieldAccess` - - **Generic template variables** — all callable join-point interfaces carry PHPStan generics to enable type-aware static analysis in aspect advice: - - `MethodInvocation` — `T` is the class holding the method (narrows `getThis()`/`getScope()`); `V` is the method return type (narrows `__invoke()` return) - - `DynamicMethodInvocation` and `StaticMethodInvocation` — subtypes with covariant narrowing (`getThis()` always returns `T` / always `null` respectively) - - `FunctionInvocation` — `V` is the function return type - - `FieldAccess` — `T` is the declaring class; `V` is the property type (narrows `getValue()`, `getValueToSet()`, `__invoke()` return) - - The proxy generators (`ClassProxyGenerator`, `TraitProxyGenerator`, `EnumProxyGenerator`, `FunctionProxyGenerator`) use `TypeGenerator::renderTypeForPhpDoc()` to extract the return type from `ReflectionMethod`/`ReflectionFunction` and emit it as the second generic argument in the per-method `@var` annotation. This gives IDE and PHPStan full type-awareness on `$__joinPoint->__invoke(...)` calls. -- `src/Aop/Framework/` — concrete invocation implementations: - - `AbstractMethodInvocation` — base class; holds `protected readonly Closure $closureToCall` (required, 4th constructor argument) — a first-class callable to the original method body; `TRAIT_ALIAS_PREFIX = '__aop__'` constant; manages recursive/cross-call stack frames - - `DynamicTraitAliasMethodInvocation` — instance-method invocation; receives `$this->__aop__method(...)` (own methods) or `parent::method(...)` (inherited) as `$closureToCall`; resolves a `private ReflectionMethod $originalMethodToCall` from the `__aop__` alias or `getPrototype()` and calls `invokeArgs($this->instance, $this->arguments)` in `proceed()` — faster than `Closure::call()` and correctly handles pass-by-reference args - - `StaticTraitAliasMethodInvocation` — static-method invocation; wraps `$closureToCall` in a `static fn(array $args) => forward_static_call($callable, ...$args)` shim and stores it in `$closureToCall`; `bindTo(null, $scope)` forwards the correct LSB class per call; `proceed()` calls `($this->closureToCall)(...$this->arguments)` - - `ReflectionConstructorInvocation` — constructor interception (used with `INTERCEPT_INITIALIZATIONS`); creates instance via `ReflectionClass::newInstanceWithoutConstructor()` then calls constructor - - `ReflectionFunctionInvocation` — function interception; receives a first-class callable to the original global function (e.g. `\strlen(...)` with leading `\` to avoid recursive proxy call); `proceed()` calls `($this->closureToCall)(...$this->arguments)` directly - - `ClassFieldAccess` — property interception join point; used via generated native property hooks on proxied properties - - `StaticInitializationJoinpoint` — fired once after proxy class is loaded via `injectJoinPoints()` -- `src/Aop/Pointcut/` — LALR pointcut grammar (`PointcutGrammar`, `PointcutParser`, `PointcutLexer`, `PointcutParseTable`) and pointcut combinators (`AndPointcut`, `OrPointcut`, `NotPointcut`, `NamePointcut`, `AttributePointcut`, etc.) -- `src/Lang/Attribute/` — PHP 8 attributes for declaring aspects and advice: `#[Aspect]`, `#[Before]`, `#[After]`, `#[Around]`, `#[AfterThrowing]`, `#[Pointcut]`, `#[DeclareError]`, `#[DeclareParents]` -- `src/Aop/Features.php` — bitmask enum for optional features (`INTERCEPT_FUNCTIONS`, `INTERCEPT_INITIALIZATIONS`, `INTERCEPT_INCLUDES`) - -### Container and aspect loading (`src/Core/`) - -- `Container.php` — DI container with `add()` (by class-string or key), `getService()`, `addLazyService()` (Closure), and automatic tagging by interface -- `AspectLoader` / `CachedAspectLoader` — scan aspect classes for pointcut/advice attributes and produce `Advisor` instances -- `AttributeAspectLoaderExtension` — handles PHP 8 attribute-based aspect definitions -- `AdviceMatcher` — given a class reflector, returns the set of applicable advisors keyed by join point; scans `IS_PUBLIC | IS_PROTECTED | IS_PRIVATE` methods (private methods from parent classes are excluded) - -### Bridge - -`src/Bridge/Doctrine/MetadataLoadInterceptor.php` — workaround for Doctrine ORM entity weaving (Doctrine loads metadata before the kernel can intercept classes). - -## PHP version support and known limitations - -The framework supports PHP 8.4+ and handles most modern PHP syntax transparently. The following constructs have documented limitations or are intentionally excluded: - -### Enums (PHP 8.1+) — woven via trait extraction - -Enums are supported by `WeavingTransformer` using the same trait-extraction approach as classes, with adjustments for enum constraints: -- The original enum body is converted to a **trait** (cases stripped, backed type removed, `enum` → `trait`) -- A proxy **enum** is generated that re-uses the trait, re-declares all cases, and adds per-method `static $__joinPoint` dispatch — using `EnumProxyGenerator` -- Enums **cannot** have properties (static or instance), so per-method `static $__joinPoint` variables are used (same pattern as `ClassProxyGenerator` and `TraitProxyGenerator`) -- Built-in enum methods (`cases`, `from`, `tryFrom`) are **never** intercepted — they are synthesised by PHP and cannot be aliased via trait use -- Built-in PHP enum interfaces (`UnitEnum`, `BackedEnum`) are **never** listed in the proxy's `implements` clause — PHP applies them automatically, and listing them explicitly in a namespaced file resolves them as `Ns\UnitEnum` instead of the global `\UnitEnum`, causing a fatal error - -### Readonly classes (PHP 8.2+) — fully supported - -When a `readonly class` is woven, the generated trait drops the `readonly` modifier (traits cannot be readonly in PHP). The proxy class **preserves** the `readonly` modifier. Method interception uses per-method function-scoped `static $__joinPoint` variables (not class properties), which are permitted in readonly classes. Properties from the original readonly class regain their implicit `readonly` status in the readonly proxy class. Readonly *properties* are excluded from `access(...)` property interception (they cannot have hooks). - -### `#[\Override]` on intercepted methods (PHP 8.3+) — attribute stripped from trait - -When a method marked `#[\Override]` is intercepted (i.e., aliased as `__aop__methodName` in the proxy's trait-use block), PHP would copy the `#[\Override]` attribute to the alias. Since the alias name has no parent method to override, PHP would raise a fatal error. `WeavingTransformer::convertClassToTrait()` therefore strips `#[\Override]` from the method body in the generated trait for every intercepted method. The attribute is **preserved on the proxy's override method**, where it is valid (the proxy extends the same parent). - -### PHP 8.4 property hooks — native property interception - -For intercepted class properties, the declaration is moved from the woven trait to the proxy class and emitted with native `get`/`set` hooks that dispatch through `ClassFieldAccess`. The woven trait has those intercepted property declarations neutralized to avoid property conflicts while preserving line numbers. - -Readonly properties and properties that already define hooks are intentionally skipped for `access(...)` interception. - -### Woven trait file line numbers must match the original source (XDebug compatibility) - -The **woven file** (the trait that replaces the original class/enum body) **must** preserve the original source line numbers. This is required for XDebug breakpoints to map correctly: a breakpoint placed at a method in the original source file must land on the same line number in the woven trait file, because that is the file XDebug steps through when executing the real method body. - -`WeavingTransformer` achieves this via token-level surgery on the original source: -- For classes: `convertClassToTrait()` replaces the `class` keyword and strips modifiers/extends/implements, but keeps all other tokens (including blank lines) in place. -- For enums: `convertEnumToTrait()` must **replace removed tokens** (case declarations, backed type, implements clause) with an **equal number of newlines** so that methods remain at their original line positions. Removing tokens without replacement shifts subsequent lines upward. - -The proxy file (generated by `ClassProxyGenerator`/`EnumProxyGenerator`) is a thin dispatch wrapper and does **not** need to match original line numbers. Debuggers will step through the woven trait for the real method bodies. - -### Aspects themselves — never woven - -Classes that implement `\Go\Aop\Aspect` are unconditionally skipped by `WeavingTransformer`. Aspects cannot weave themselves. - -## Test conventions - -- Tests mirror the `src/` structure under `tests/Go/` -- Functional/integration tests live in `tests/Go/Functional/` -- Test fixtures (stub classes for weaving) live in `tests/Go/Stubs/` and `tests/Fixtures/project/src/` (autoloaded as `Go\Tests\TestProject\`) -- Snapshot fixtures for `WeavingTransformerTest` live in `tests/Go/Instrument/Transformer/_files/`; `*-woven.php` is the transformed source (class→trait), `*-proxy.php` is the generated proxy -- PHPUnit 13+, bootstrap is `vendor/autoload.php` (no separate test bootstrap) -- PHPStan level 10 is a mandatory gate — run `./vendor/bin/phpstan analyze --memory-limit=512M` before every commit diff --git a/src/Aop/AGENTS.md b/src/Aop/AGENTS.md new file mode 100644 index 00000000..b58e5502 --- /dev/null +++ b/src/Aop/AGENTS.md @@ -0,0 +1,52 @@ +# src/Aop — Aspect-Oriented Programming core + +## Interfaces (src/Aop/Intercept/) + +### Joinpoint hierarchy +Joinpoint → Invocation +Joinpoint → ClassJoinpoint +Invocation → MethodInvocation +Invocation → ConstructorInvocation +Invocation → FunctionInvocation +ClassJoinpoint → FieldAccess +ClassJoinpoint → MethodInvocation +ClassJoinpoint → ConstructorInvocation + +### Generics (PHPStan type-awareness) +| Interface | Generic | T= | V= | +|-------------------------|--------------------------|----------------------------|----------------| +| MethodInvocation | `` | class holding method | return type | +| DynamicMethodInvocation | `` | getThis()→T (covariant) | return type | +| StaticMethodInvocation | `` | getThis()→null (covariant) | return type | +| FunctionInvocation | `` | — | return type | +| FieldAccess | `` | class holding property | property type | +| ConstructorInvocation | `` | class being constructed | — | + +Proxy generators use TypeGenerator::renderTypeForPhpDoc() to emit V as 2nd generic arg in per-method @var annotations — gives IDE/PHPStan full type-awareness on $__joinPoint->__invoke(). + +## Implementations (src/Aop/Framework/) +| Class | Implements | Key behavior | +|-----------------------------------|-------------------------|------------------------------------------------------------------------------------------------------------------------| +| AbstractMethodInvocation | MethodInvocation | Base; protected readonly Closure $closureToCall (FCC); TRAIT_ALIAS_PREFIX='__aop__'; keeps method reflection | +| DynamicTraitAliasMethodInvocation | DynamicMethodInvocation | receives $this->__aop__m(...) or parent::m(...); proceed() via ReflectionMethod::invokeArgs (handles by-ref correctly) | +| StaticTraitAliasMethodInvocation | StaticMethodInvocation | FCC shim: static fn(array $args) => forward_static_call_array(...); bindTo(null, $scope) per call | +| ReflectionConstructorInvocation | ConstructorInvocation | newInstanceWithoutConstructor() then call constructor (requires INTERCEPT_INITIALIZATIONS feature) | +| ReflectionFunctionInvocation | FunctionInvocation | receives FCC to global fn (e.g. \strlen(...) with leading \ to avoid recursive proxy call) | +| ClassFieldAccess | FieldAccess | Property interception via native get/set hooks on proxied properties | +| StaticInitializationJoinpoint | ClassJoinpoint | Fired once after proxy class loaded via injectJoinPoints() | + +## Pointcuts (src/Aop/Pointcut/) +- LALR grammar: PointcutGrammar, PointcutParser, PointcutLexer, PointcutParseTable +- Combinators: AndPointcut, OrPointcut, NotPointcut, NamePointcut, AttributePointcut, ClassInheritancePointcut, MatchInheritedPointcut, ModifierPointcut, ReturnTypePointcut, MagicMethodDynamicPointcut, TruePointcut +- PointcutReference, ClassMemberReference + +## Attributes (src/Lang/Attribute/) +- Advice: #[Before], #[After], #[Around], #[AfterThrowing] +- Declaration: #[Aspect], #[Pointcut], #[DeclareError], #[DeclareParents] +- Base: AbstractAttribute, AbstractInterceptor, Interceptor (interface) + +## Features (src/Aop/Features.php) +Interface with bitmask constants: +- INTERCEPT_FUNCTIONS=1, INTERCEPT_INITIALIZATIONS=2, INTERCEPT_INCLUDES=4 +- PREBUILT_CACHE=64 — assume cache already prepared, skip freshness checks +- PARAMETER_WIDENING=128 — enable parameter widening for PHP>=7.2 diff --git a/src/Core/AGENTS.md b/src/Core/AGENTS.md new file mode 100644 index 00000000..dd6ffd2b --- /dev/null +++ b/src/Core/AGENTS.md @@ -0,0 +1,15 @@ +# src/Core — Container and aspect loading + +## Container (Container.php) +- DI container: add(by class-string|key), getService(), addLazyService(Closure) +- Automatic tagging by interface + +## Aspect loading +- AspectLoader / CachedAspectLoader — scan aspect classes for pointcut/advice attributes → Advisor[] +- AttributeAspectLoaderExtension — handles PHP 8 attribute-based aspect definitions +- AdviceMatcher — given class reflector, returns applicable advisors keyed by join point + - Scans IS_PUBLIC|IS_PROTECTED|IS_PRIVATE methods + - Private methods from parent classes excluded + +## Bridge +src/Bridge/Doctrine/MetadataLoadInterceptor.php — workaround for Doctrine ORM entity weaving (Doctrine loads metadata before kernel can intercept classes). diff --git a/src/Instrument/AGENTS.md b/src/Instrument/AGENTS.md new file mode 100644 index 00000000..f90b903d --- /dev/null +++ b/src/Instrument/AGENTS.md @@ -0,0 +1,64 @@ +# src/Instrument — AOP interception pipeline + +## Init flow +1. AspectKernel::init() — singleton, registers stream filter, builds transformers, calls configureAop() +2. SourceTransformingLoader::register() — PHP stream filter (php://filter/read=go.source.transforming.loader/resource=... protocol) +3. AopComposerLoader::init() — hooks Composer autoloader → redirects through stream filter +4. CachingTransformer — outer; cache miss → inner chain → write cache + +## Transformer chain (order matters) +Applied per loaded file. Each returns TransformerResultEnum: RESULT_TRANSFORMED|RESULT_ABSTAIN|RESULT_ABORTED. + +1. ConstructorExecutionTransformer — new expressions (works only if INTERCEPT_INITIALIZATIONS enabled) +2. FilterInjectorTransformer — include/require (works only if INTERCEPT_INCLUDES enabled) +3. WeavingTransformer — main; AdviceMatcher + CachedAspectLoader → proxy generators +4. MagicConstantTransformer — `__FILE__`/`__DIR__` → original paths + +## Trait-based proxy engine (4.0) +WeavingTransformer converts original class to trait + proxy class. Two generated files for class Ns\Foo: + +### Woven file (replaces original in php stream filtering) +```php +trait Foo__AopProxied { /* original methods verbatim */ } +include_once AOP_CACHE_DIR . '/_proxies/.../Foo.php'; +``` + +### Proxy file (loaded by include_once) +```php +class Foo extends OriginalParent implements OriginalInterfaces, \Go\Aop\Proxy +{ + use \Ns\Foo__AopProxied { + \Ns\Foo__AopProxied::interceptedMethod as private __aop__interceptedMethod; + } + public function interceptedMethod(ArgType $arg): ReturnType { + /** @var \Go\Aop\Intercept\DynamicMethodInvocation $__joinPoint */ + static $__joinPoint = \Go\Aop\Framework\InterceptorInjector::forMethod( + self::class, 'interceptedMethod', [...], $this->__aop__interceptedMethod(...) + ); + return $__joinPoint->__invoke($this, [$arg]); + } +} +``` + +### Key invariants +- Proxy re-inherits parent+interfaces via reflection (not from woven source) +- self:: in trait body → proxy class (no rewrite needed) +- Private methods interceptable (impossible with old extend-based engine) +- FCC 4th arg to InterceptorInjector: + - `$this->__aop__m(...)` — own dynamic methods + - `self::__aop__m(...)` — own static methods + - `parent::m(...)` — inherited methods (no trait alias) + - `\fn(...)` — function proxies + +## Line preservation: Woven trait line numbers (XDebug) +Woven trait MUST preserve original source line numbers for XDebug breakpoints. +- Class→trait: convertClassToTrait() replaces `class` keyword, strips modifiers/extends/implements. All other tokens (incl. blank lines) kept in place. +- Enum→trait: convertEnumToTrait() replaces removed tokens (cases, backed type, implements) with equal number of newlines to keep methods at original line positions. +- Proxy file (ClassProxyGenerator/EnumProxyGenerator): thin dispatch wrapper — line numbers don't matter. + +## PHP compat: #[\Override] on intercepted methods (8.3+) +When intercepted method has #[\Override], PHP copies attribute to trait alias — fatal error (alias doesn't override anything). +WeavingTransformer::convertClassToTrait() strips #[\Override] from trait for every intercepted method. Attribute preserved on proxy's override method (proxy extends same parent). + +## Aspects themselves +Classes implementing \Go\Aop\Aspect: unconditionally skipped by WeavingTransformer. Aspects cannot weave themselves. diff --git a/src/Proxy/AGENTS.md b/src/Proxy/AGENTS.md new file mode 100644 index 00000000..d4f76602 --- /dev/null +++ b/src/Proxy/AGENTS.md @@ -0,0 +1,39 @@ +# src/Proxy — Proxy generation + +## Proxy generators +- ClassProxyGenerator — trait-based proxy for regular classes + - Constructor takes $traitName (Foo__AopProxied FQCN) as 2nd arg + - Always emits `use $traitName` (even for introduction-only aspects) +- FunctionProxyGenerator — function wrappers +- TraitProxyGenerator — trait proxies +- EnumProxyGenerator — enum proxies (trait extraction + case re-declaration) + +## Proxy parts (src/Proxy/Part/) +- InterceptedMethodGenerator — wraps a method with join-point dispatch +- InterceptedConstructorGenerator — wraps constructor + - Calls `$this->__aop____construct()` when constructor is in trait, `parent::__construct()` otherwise +- InterceptedPropertyGenerator — re-declares properties with native get/set hooks → ClassFieldAccess + +## Generators (src/Proxy/Generator/) +- ClassGenerator — builds proxy class AST + - addTraitAlias() registers trait + alias in single `use { ... }` block; deduplicates traits +- AttributeGroupsGenerator — copies PHP 8 attributes from reflection to proxy AST (preserves named args) +- TypeGenerator — converts ReflectionType to AST nodes or phpDoc strings + - renderTypeForPhpDoc() used by all proxy generators for @var generic on $__joinPoint + +## PHP compat: Readonly classes (8.2+) +- Generated trait drops `readonly` (traits can't be readonly in PHP) +- Proxy class preserves `readonly` +- Per-method function-scoped static $__joinPoint (not class property) — allowed in readonly classes +- Readonly properties excluded from access() interception (can't have hooks) + +## PHP compat: Property hooks (8.4+) +- Intercepted properties: moved from trait to proxy, emitted with get/set hooks dispatching through ClassFieldAccess +- Woven trait: property declarations neutralized (avoid conflicts, preserve line numbers) +- Readonly properties and properties with existing hooks: skipped for access() interception + +## PHP compat: Enum proxies +- Original enum → trait (cases stripped, backed type removed, enum→trait) +- Proxy enum re-uses trait, re-declares all cases, per-method static $__joinPoint +- Built-in enum methods (cases/from/tryFrom): NEVER intercepted (PHP-synthesised, can't alias via trait use) +- UnitEnum/BackedEnum: NEVER in proxy implements (PHP auto-applies; explicit listing resolves as Ns\UnitEnum → fatal error) diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 00000000..a846d7f5 --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,17 @@ +# tests — Test conventions + +## Structure +- Test categories mirror src/ namespaces (e.g. tests/Core/ for src/Core/, tests/Proxy/ for src/Proxy/). +- Functional/integration: tests/Functional/ +- Test fixtures (stub classes for weaving): tests/Stubs/, tests/Fixtures/project/src/ (autoloaded as Go\Tests\TestProject\) +- Snapshot fixtures: tests/Instrument/Transformer/_files/ (*-woven.php trait, *-proxy.php proxy) + +## PHPUnit +- Mandatory before commit +- Version: 13+ +- If phpstan fails: fix errors before offering to commit + +## PHPStan gate +- Mandatory before commit +- `./vendor/bin/phpstan analyze --memory-limit=512M` +- If phpstan fails: fix errors before offering to commit \ No newline at end of file