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
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ Changelog
* [BC BREAK] Requires PHP 8.4+
* [Feature] **First-class callable advices** — the main way advices are now wired into woven code. Generated proxies declare each aspect-method advice as `Interceptor::before(The::aspect(MonitorAspect::class)->beforeMethodExecution(...))` — an eager first-class callable, since the interceptor list is only built while the intercepted method or hook is already executing (`LazyAdvisorAccessor` is removed). Compiled advisor cache files use the same facade in its lazy static-data form, e.g. `Interceptor::before(MonitorAspect::class, 'beforeMethodExecution')`, which returns a native PHP lazy proxy: interceptor construction, aspect resolution from the container and advice callable creation are all deferred until the advice is actually used, so cached advices whose pointcut never matches never instantiate their aspect. Advices registered in the container as plain closures (not aspect methods) are resolved through the new `The::advice('advisorId')` accessor, which unwraps `Advisor` and interceptor values down to the raw advice closure.
* [BC BREAK] **Aspect advice methods must be public.** Because generated proxies call advices as first-class callables on the aspect instance, an advice method annotated with `#[Before]`, `#[After]`, `#[Around]` or `#[AfterThrowing]` can no longer be `protected` or `private` — the aspect loader now throws an `AspectException` for non-public advice methods. Methods holding only a `#[Pointcut]` attribute may keep any visibility.
* [BC BREAK] Generated code no longer uses the `__aop__` prefix. The trait holding the original class body is now suffixed with `OriginalTrait` (`FooOriginalTrait` instead of `Foo__AopProxied`) and the private trait aliases that back the original method bodies carry their own `OriginalAlias` suffix (`doSomethingOriginalAlias` instead of `__aop__doSomething`). Accordingly `AbstractMethodInvocation::TRAIT_ALIAS_PREFIX` is replaced by `AbstractMethodInvocation::TRAIT_ALIAS_SUFFIX` (`'OriginalAlias'`), `AspectContainer::AOP_PROXIED_SUFFIX` is now `'OriginalTrait'`, and the hooks declared by `InitializationAware` and `StaticInitializationAware` are renamed to `__initialization()` and `__staticInitialization()`. Cached proxies generated by an earlier 4.0 development version must be regenerated.
* [BC BREAK] Removed the `AdviceBefore`, `AdviceAfter` and `AdviceAround` marker interfaces. The `Advice` interface now requires `getType(): AdviceTypeEnum`, and the new `AdviceTypeEnum` backed enum (`Before`, `After`, `AfterThrowing`, `Around`, `Introduction`) carries both the advice kind and its invocation priority used for joinpoint sorting.
* [BC BREAK] Proxy engine switched from inheritance-based to **trait-based**: the original class body is converted to a PHP trait (`Foo__AopProxied`) and the proxy class uses it via `use` with private method aliases instead of extending the renamed class. This removes the `__AopProxied` parent from the inheritance chain.
* [BC BREAK] All invocation class constructors (`DynamicTraitAliasMethodInvocation`, `StaticTraitAliasMethodInvocation`, `ReflectionFunctionInvocation`) now require a `Closure $closureToCall` parameter (non-nullable). Generated proxy code always passes a first-class callable: `$this->__aop__method(...)` for own instance methods, `self::__aop__method(...)` for own static methods, `parent::method(...)` for inherited methods, and `\functionName(...)` for functions.
* [BC BREAK] Proxy engine switched from inheritance-based to **trait-based**: the original class body is converted to a PHP trait (`FooOriginalTrait`) and the proxy class uses it via `use` with private method aliases instead of extending the renamed class. This removes the renamed `<Class>OriginalTrait` parent from the inheritance chain.
* [BC BREAK] All invocation class constructors (`DynamicTraitAliasMethodInvocation`, `StaticTraitAliasMethodInvocation`, `ReflectionFunctionInvocation`) now require a `Closure $closureToCall` parameter (non-nullable). Generated proxy code always passes a first-class callable: `$this->methodOriginalAlias(...)` for own instance methods, `self::methodOriginalAlias(...)` for own static methods, `parent::method(...)` for inherited methods, and `\functionName(...)` for functions.
* [Feature] **Private method interception** — both dynamic (`private function foo()`) and static (`private static function bar()`) private methods can now be intercepted by aspects. This was impossible with the old extend-based engine because PHP does not allow overriding private methods in subclasses.
* [Feature] **PHP 8.5+ first-class callable default values** — proxy generation for method parameters and constructor-promoted properties now supports `Closure` default values expressed via first-class callable syntax (e.g., `function foo($cb = strlen(...))`). The raw AST expression node is preserved through the proxy-generation pipeline so the generated source code reproduces the original FCC default verbatim.
* [Feature] **PHP 8.1+ enum interception** — instance and static methods on both unit (pure) and backed enums can now be intercepted by aspects. The enum body is extracted into a trait (`Foo__AopProxied`); a proxy enum re-declares the cases and dispatches intercepted methods via per-method `static $__joinPoint` caching. Built-in enum methods (`cases`, `from`, `tryFrom`) and initialization joinpoints are never woven.
* [Feature] **PHP 8.1+ enum interception** — instance and static methods on both unit (pure) and backed enums can now be intercepted by aspects. The enum body is extracted into a trait (`FooOriginalTrait`); a proxy enum re-declares the cases and dispatches intercepted methods via per-method `static $__joinPoint` caching. Built-in enum methods (`cases`, `from`, `tryFrom`) and initialization joinpoints are never woven.
* [Feature] `self::` in proxied classes now resolves to the proxy class naturally (via PHP trait semantics), removing the need for `SelfValueTransformer`.
* [Feature] **First-class callable syntax** — generated proxy code and invocation constructors use PHP 8.1+ first-class callable syntax (`$this->__aop__method(...)`, `parent::method(...)`, `\func(...)`) to reference original method and function bodies, eliminating the need for `Closure::bind` at construction time.
* [Feature] **First-class callable syntax** — generated proxy code and invocation constructors use PHP 8.1+ first-class callable syntax (`$this->methodOriginalAlias(...)`, `parent::method(...)`, `\func(...)`) to reference original method and function bodies, eliminating the need for `Closure::bind` at construction time.
* [BC BREAK] Removed `Features::PARAMETER_WIDENING` and the parameter-widening code path in the proxy generators. The feature was a PHP 7.0/7.1 compatibility aid (parameter type widening, wiki.php.net/rfc/parameter-no-type-variance) that has been a no-op concern since the PHP 7.2 baseline: generated proxies always keep the original parameter types. Remove the flag from `AspectKernel::configureAop()` options if you passed it.
* [BC BREAK] Removed DeclareError support, including the `DeclareError` attribute, `DeclareErrorInterceptor`, and `PointcutBuilder::declareError()`. Use `Before` or `Around` interceptors to emit user warnings or throw exceptions instead.
* [BC BREAK] Removed support for the "dynamic" pointcut (`dynamic(public Foo->method*(*))`), including `MagicMethodDynamicPointcut`, `DynamicInvocationMatcherInterceptor`, the `Pointcut::KIND_DYNAMIC` constant and the `$instanceOrScope`/`$arguments` parameters of `Pointcut::matches()`. Use a traditional execution pointcut for the magic methods instead, e.g. `execution(public Foo->__call(*))` or `execution(public Foo::__callStatic(*))`, and check the invoked method name from `$invocation->getArguments()[0]` inside the advice.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ static $__joinPoint = InterceptorInjector::forMethod(
[
Interceptor::before(The::aspect(MonitorAspect::class)->beforeMethodExecution(...)),
],
$this->__aop__doSomething(...),
$this->doSomethingOriginalAlias(...),
);
```

Expand Down
2 changes: 1 addition & 1 deletion docs/php85-limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Several of the limitations below are actively being worked on — they are phras
### Pipe operator `|>`

The [pipe operator](https://wiki.php.net/rfc/pipe-operator-v3) works inside woven method bodies.
Method bodies are moved verbatim into the `__AopProxied` trait, so any PHP 8.5 expression syntax
Method bodies are moved verbatim into the `<Class>OriginalTrait` body trait, so any PHP 8.5 expression syntax
inside them is preserved.

### `clone with`
Expand Down
12 changes: 6 additions & 6 deletions phpstan-baseline-tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
$ignoreErrors = [];

// MetadataLoadInterceptorTest deliberately builds Doctrine ClassMetadata for
// fake "__AopProxied" class names with intentionally malformed mapping data,
// fake "OriginalTrait" class names with intentionally malformed mapping data,
// and then asserts that MetadataLoadInterceptor::loadClassMetadata() reset it
// at runtime. Static analysis can neither accept the fake class-strings nor
// see the interceptor's mutations, so every finding below is a false positive
Expand All @@ -18,31 +18,31 @@
'path' => __DIR__ . '/tests/Aop/Bridge/Doctrine/MetadataLoadInterceptorTest.php',
];
$ignoreErrors[] = [
'message' => '#^Parameter \\#1 \\$name of class Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata constructor expects class\\-string\\<__AopProxied\\\\Some\\\\Class\\\\Name\\>, string given\\.$#',
'message' => '#^Parameter \\#1 \\$name of class Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata constructor expects class\\-string\\<OriginalTrait\\\\Some\\\\Class\\\\Name\\>, string given\\.$#',
'identifier' => 'argument.type',
'count' => 1,
'path' => __DIR__ . '/tests/Aop/Bridge/Doctrine/MetadataLoadInterceptorTest.php',
];
$ignoreErrors[] = [
'message' => '#^Parameter \\#1 \\$name of class Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata constructor expects class\\-string\\<__AopProxied\\>, string given\\.$#',
'message' => '#^Parameter \\#1 \\$name of class Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata constructor expects class\\-string\\<OriginalTrait\\>, string given\\.$#',
'identifier' => 'argument.type',
'count' => 1,
'path' => __DIR__ . '/tests/Aop/Bridge/Doctrine/MetadataLoadInterceptorTest.php',
];
$ignoreErrors[] = [
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\<Go\\\\Aop\\\\Bridge\\\\Doctrine\\\\Entity__AopProxied\\>\\:\\:\\$table \\(array\\{name\\: string, schema\\?\\: string, indexes\\?\\: array, uniqueConstraints\\?\\: array, options\\?\\: array\\<string, mixed\\>, quoted\\?\\: bool\\}\\) does not accept array\\{\'table_name\'\\}\\.$#',
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\<Go\\\\Aop\\\\Bridge\\\\Doctrine\\\\EntityOriginalTrait\\>\\:\\:\\$table \\(array\\{name\\: string, schema\\?\\: string, indexes\\?\\: array, uniqueConstraints\\?\\: array, options\\?\\: array\\<string, mixed\\>, quoted\\?\\: bool\\}\\) does not accept array\\{\'table_name\'\\}\\.$#',
'identifier' => 'assign.propertyType',
'count' => 1,
'path' => __DIR__ . '/tests/Aop/Bridge/Doctrine/MetadataLoadInterceptorTest.php',
];
$ignoreErrors[] = [
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\<Go\\\\Aop\\\\Bridge\\\\Doctrine\\\\Entity__AopProxied\\>\\:\\:\\$customRepositoryClassName \\(class\\-string\\<Doctrine\\\\ORM\\\\EntityRepository\\>\\|null\\) does not accept string\\.$#',
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\<Go\\\\Aop\\\\Bridge\\\\Doctrine\\\\EntityOriginalTrait\\>\\:\\:\\$customRepositoryClassName \\(class\\-string\\<Doctrine\\\\ORM\\\\EntityRepository\\>\\|null\\) does not accept string\\.$#',
'identifier' => 'assign.propertyType',
'count' => 1,
'path' => __DIR__ . '/tests/Aop/Bridge/Doctrine/MetadataLoadInterceptorTest.php',
];
$ignoreErrors[] = [
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\<Go\\\\Aop\\\\Bridge\\\\Doctrine\\\\Entity__AopProxied\\>\\:\\:\\$fieldMappings \\(array\\<string, Doctrine\\\\ORM\\\\Mapping\\\\FieldMapping\\>\\) does not accept array\\<string, array\\<string, string\\>\\|Doctrine\\\\ORM\\\\Mapping\\\\FieldMapping\\>\\.$#',
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\<Go\\\\Aop\\\\Bridge\\\\Doctrine\\\\EntityOriginalTrait\\>\\:\\:\\$fieldMappings \\(array\\<string, Doctrine\\\\ORM\\\\Mapping\\\\FieldMapping\\>\\) does not accept array\\<string, array\\<string, string\\>\\|Doctrine\\\\ORM\\\\Mapping\\\\FieldMapping\\>\\.$#',
'identifier' => 'assign.propertyType',
'count' => 1,
'path' => __DIR__ . '/tests/Aop/Bridge/Doctrine/MetadataLoadInterceptorTest.php',
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Proxy generators use TypeGenerator::renderTypeForPhpDoc() to emit V as 2nd gener
## 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) |
| AbstractMethodInvocation | MethodInvocation | Base; protected readonly Closure $closureToCall (FCC); TRAIT_ALIAS_SUFFIX='OriginalAlias'; keeps method reflection |
| DynamicTraitAliasMethodInvocation | DynamicMethodInvocation | receives $this->mOriginalAlias(...) 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) |
Expand Down
6 changes: 3 additions & 3 deletions src/Aop/Framework/AbstractMethodInvocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
abstract class AbstractMethodInvocation extends AbstractInvocation implements MethodInvocation
{
/**
* Prefix used for trait method aliases that back the original method body.
* The proxy class aliases each intercepted method as `private __aop__<method>` in the trait-use block.
* Suffix used for trait method aliases that back the original method body.
* The proxy class aliases each intercepted method as `private <method>OriginalAlias` in the trait-use block.
*/
public const string TRAIT_ALIAS_PREFIX = '__aop__';
public const string TRAIT_ALIAS_SUFFIX = 'OriginalAlias';

protected readonly ReflectionMethod $reflectionMethod;

Expand Down
6 changes: 3 additions & 3 deletions src/Aop/Framework/DynamicTraitAliasMethodInvocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* Dynamic trait-alias method invocation calls instance methods via reflection.
*
* The callable is provided by the generated proxy code and points to the original method body:
* - For methods declared in the proxied class: `$this->__aop__<method>(...)` — the private
* - For methods declared in the proxied class: `$this-><method>OriginalAlias(...)` — the private
* alias created in the proxy's trait-use block.
* - For inherited methods (no trait alias): `parent::<method>(...)`.
*
Expand Down Expand Up @@ -54,7 +54,7 @@ final class DynamicTraitAliasMethodInvocation extends AbstractMethodInvocation i

/**
* ReflectionMethod pointing to the original method body:
* - For methods with a trait alias: the private `__aop__<method>` alias.
* - For methods with a trait alias: the private `<method>OriginalAlias` alias.
* - For inherited methods without a trait alias: the prototype method from the parent class.
*/
private readonly ReflectionMethod $originalMethodToCall;
Expand All @@ -64,7 +64,7 @@ final class DynamicTraitAliasMethodInvocation extends AbstractMethodInvocation i
* @param class-string<T> $className Class, containing method to invoke
* @param non-empty-string $methodName Name of the method to invoke
* @param Closure $closureToCall First-class callable to the original method body,
* e.g. `$this->__aop__method(...)` for trait-aliased
* e.g. `$this->methodOriginalAlias(...)` for trait-aliased
* methods or `parent::method(...)` for inherited ones.
*/
public function __construct(array $advices, string $className, string $methodName, Closure $closureToCall)
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Framework/InterceptorInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class InterceptorInjector
* @param non-empty-string $methodName
* @param non-empty-list<Interceptor> $interceptors
* @param Closure $closureToCall First-class callable to the original method body,
* e.g. `$this->__aop__method(...)` for trait-aliased methods or
* e.g. `$this->methodOriginalAlias(...)` for trait-aliased methods or
* `parent::method(...)` for inherited methods.
* @return DynamicMethodInvocation<T>
*/
Expand All @@ -52,7 +52,7 @@ public static function forMethod(string $className, string $methodName, array $i
* @param non-empty-string $methodName
* @param non-empty-list<Interceptor> $interceptors
* @param Closure $closureToCall First-class callable to the original static method body,
* e.g. `self::__aop__method(...)` for trait-aliased methods or
* e.g. `self::methodOriginalAlias(...)` for trait-aliased methods or
* `parent::method(...)` for inherited methods.
* @return StaticMethodInvocation<T>
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Framework/StaticTraitAliasMethodInvocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* that is rebound to each caller's late-static-binding scope on invocation.
*
* The callable is provided by the generated proxy code and points to the original method body:
* - For methods declared in the proxied class: `self::__aop__<method>(...)` — the private
* - For methods declared in the proxied class: `self::<method>OriginalAlias(...)` — the private
* alias created in the proxy's trait-use block.
* - For inherited methods (no trait alias): `parent::<method>(...)`.
*
Expand Down Expand Up @@ -57,7 +57,7 @@ final class StaticTraitAliasMethodInvocation extends AbstractMethodInvocation im
* @param class-string<T> $className Class, containing method to invoke
* @param non-empty-string $methodName Name of the method to invoke
* @param Closure $closureToCall First-class callable to the original static method body,
* e.g. `self::__aop__method(...)` or `parent::method(...)`.
* e.g. `self::methodOriginalAlias(...)` or `parent::method(...)`.
*/
public function __construct(array $advices, string $className, string $methodName, Closure $closureToCall)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aop/InitializationAware.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ interface InitializationAware
* @param list<mixed> $arguments
* @return T
*/
public static function __aop__initialization(array $arguments = []): object;
public static function __initialization(array $arguments = []): object;
}
2 changes: 1 addition & 1 deletion src/Aop/StaticInitializationAware.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
*/
interface StaticInitializationAware
{
public static function __aop__staticInitialization(): void;
public static function __staticInitialization(): void;
}
Loading
Loading