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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Changelog
* [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.
* [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.
* [Removed] `SelfValueTransformer` and `SelfValueVisitor` — no longer needed with the trait-based engine.
* [Performance] **Direct static joinpoint initialization** — leveraging PHP 8.3+ support for dynamic expressions in static variable initializers, all generated proxy method bodies now initialize their static joinpoint variables directly.

Expand Down
21 changes: 15 additions & 6 deletions demos/Demo/Aspect/DynamicMethodsAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,27 @@
use Go\Lang\Attribute\Before;

/**
* Aspect that intercepts specific magic methods, declared with __call and __callStatic
* Aspect that intercepts magic methods, declared with __call and __callStatic
*
* Traditional "execution" pointcuts match the magic method itself, so the real method name
* should be extracted from the invocation arguments and filtered inside the advice.
*/
class DynamicMethodsAspect implements Aspect
{
/**
* This advice intercepts an execution of __call methods
* This advice intercepts an execution of __call method
*
* Unlike traditional "execution" pointcut, "dynamic" is checking the name of method in
* the runtime, allowing to write interceptors for __call more transparently.
* The name of the invoked method is the first invocation argument,
* so we filter interesting methods (save*) right inside the advice.
*/
#[Before('dynamic(public Demo\Example\DynamicMethodsDemo->save*(*))')]
#[Before('execution(public Demo\Example\DynamicMethodsDemo->__call(*))')]
public function beforeMagicMethodExecution(MethodInvocation $invocation): void
{
// we need to unpack args from invocation args
[$methodName, $args] = $invocation->getArguments();
if (!str_starts_with($methodName, 'save')) {
return;
}
echo 'Calling Magic Interceptor for method: ',
$invocation->getScope(),
'->',
Expand All @@ -45,11 +51,14 @@ public function beforeMagicMethodExecution(MethodInvocation $invocation): void
/**
* This advice intercepts an execution of methods via __callStatic
*/
#[Before('dynamic(public Demo\Example\DynamicMethodsDemo::find*(*))')]
#[Before('execution(public Demo\Example\DynamicMethodsDemo::__callStatic(*))')]
public function beforeMagicStaticMethodExecution(MethodInvocation $invocation): void
{
// we need to unpack args from invocation args
[$methodName, $args] = $invocation->getArguments();
if (!str_starts_with($methodName, 'find')) {
return;
}
echo 'Calling Static Magic Interceptor for method: ',
$invocation->getScope(),
'::',
Expand Down
2 changes: 1 addition & 1 deletion demos/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@

$example = new DynamicMethodsDemo();
$example->saveById(123); // intercept magic dynamic method
$example->load(456); // notice, that advice for this magic method is not called
$example->load(456); // notice, that advice filters out this method by name
DynamicMethodsDemo::find(['id' =>124]); //intercept magic static method
break;

Expand Down
2 changes: 1 addition & 1 deletion src/Aop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Proxy generators use TypeGenerator::renderTypeForPhpDoc() to emit V as 2nd gener

## Pointcuts (src/Aop/Pointcut/)
- LALR grammar: PointcutGrammar, PointcutParser, PointcutLexer, PointcutParseTable
- Combinators: AndPointcut, OrPointcut, NotPointcut, NamePointcut, AttributePointcut, ClassInheritancePointcut, MatchInheritedPointcut, ModifierPointcut, ReturnTypePointcut, MagicMethodDynamicPointcut, TruePointcut
- Combinators: AndPointcut, OrPointcut, NotPointcut, NamePointcut, AttributePointcut, ClassInheritancePointcut, MatchInheritedPointcut, ModifierPointcut, ReturnTypePointcut, TruePointcut
- PointcutReference, ClassMemberReference

## Attributes (src/Lang/Attribute/)
Expand Down
50 changes: 0 additions & 50 deletions src/Aop/Framework/DynamicInvocationMatcherInterceptor.php

This file was deleted.

28 changes: 6 additions & 22 deletions src/Aop/Pointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@
use ReflectionProperty;

/**
* Pointcut is responsible for matching any reflection items both statically and dynamically.
* Pointcut is responsible for matching any reflection items statically.
*
* Pointcut may be evaluated statically or at runtime (dynamically).
* Matcher uses smart technique of matching elements, consisting of several stages described below.
*
* <b>Static matching</b>
*
* First stage of static matching involves context only (just one argument). This pre-stage is used to optimize
* First stage of matching involves context only (just one argument). This pre-stage is used to optimize
* filtering on matcher side to avoid nested loops of checks. For example, if we have a method pointcut, but
* it doesn't match first with class, then we don't need to scan all methods at all and can exit earlier.
*
Expand All @@ -35,21 +32,13 @@
* - For any functions, context will be `ReflectionFileNamespace` where internal function is analyzed.
* - For any methods or properties, context will be `ReflectionClass` which is currently analysed (even for inherited items)
*
* Second stage of static matching uses exactly two arguments (context and reflector). Filter then fully checks
* Second stage of matching uses exactly two arguments (context and reflector). Filter then fully checks
* static information from reflection to make a decision about matching of given point.
*
* At this stage we can verify names, attributes, signature, parameters, types, etc.
*
* If point filter is not dynamic {@see self::KIND_DYNAMIC}, then evaluation ends here statically,
* and generated code will not contain any runtime checks for given point filter, allowing for better performance.
*
* <b>Dynamic matching</b>
*
* If instance of filter is dynamic and uses {@see self::KIND_DYNAMIC} flag, then after static matching which has been
* used to prepare a dynamic hook, framework will call our pointcut again in runtime for dynamic matching.
*
* This dynamic matching stage uses full information about given join point, including possible instance/scope and
* arguments for a particular point.
* Evaluation ends here statically, and generated code will not contain any runtime checks
* for given point filter, allowing for better performance.
*/
interface Pointcut
{
Expand All @@ -61,7 +50,6 @@ interface Pointcut
public const KIND_INIT = 32;
public const KIND_STATIC_INIT = 64;
public const KIND_ALL = 127;
public const KIND_DYNAMIC = 256;
public const KIND_INTRODUCTION = 512;

/**
Expand All @@ -74,15 +62,11 @@ public function getKind(): int;
*
* @param ReflectionClass<T>|ReflectionFileNamespace $context Related context, can be class or file namespace
* @param ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector Specific part of code, can be any Reflection class
* @param null|class-string<T>|T $instanceOrScope Invocation instance or string for static calls
* @param null|array<mixed> $arguments Dynamic arguments for method
*
* @template T of object
*/
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool;
}
6 changes: 2 additions & 4 deletions src/Aop/Pointcut/AndPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ public function __construct(?int $pointcutKind = null, Pointcut ...$pointcuts)

public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool {
foreach ($this->pointcuts as $singlePointcut) {
if (!$singlePointcut->matches($context, $reflector, $instanceOrScope, $arguments)) {
if (!$singlePointcut->matches($context, $reflector)) {
return false;
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/Aop/Pointcut/AttributePointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ public function __construct(

final public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool {
// If we don't use context for matching and we do static check, then always match
if (!$this->useContextForMatching && !isset($reflector)) {
Expand Down
4 changes: 1 addition & 3 deletions src/Aop/Pointcut/ClassInheritancePointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ public function __construct(private string $parentClassOrInterfaceName) {}

public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool {
// We match only with ReflectionClass as a context
if (!$context instanceof ReflectionClass) {
Expand Down
93 changes: 0 additions & 93 deletions src/Aop/Pointcut/MagicMethodDynamicPointcut.php

This file was deleted.

4 changes: 1 addition & 3 deletions src/Aop/Pointcut/MatchInheritedPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ final class MatchInheritedPointcut implements Pointcut
{
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool {
// Inherited items can be only inside class context
if (!$context instanceof ReflectionClass) {
Expand Down
4 changes: 1 addition & 3 deletions src/Aop/Pointcut/ModifierPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ public function __construct(int $initialMask = 0)
*/
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool {
// With context only we always match, as we don't know about modifiers of given reflector
if (!isset($reflector)) {
Expand Down
4 changes: 1 addition & 3 deletions src/Aop/Pointcut/NamePointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ public function __construct(

public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool {
// Let's determine what will be used for matching - context or reflector
if ($this->useContextForMatching) {
Expand Down
8 changes: 3 additions & 5 deletions src/Aop/Pointcut/NotPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,15 @@ public function __construct(private Pointcut $pointcut) {}
*/
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool {
// For Logical "not" expression without reflector, we should match statically for any context
if (!isset($reflector)) {
return true;
}

// Otherwise we return inverted result from static/dynamic matching
return !$this->pointcut->matches($context, $reflector, $instanceOrScope, $arguments);
// Otherwise we return inverted result from static matching
return !$this->pointcut->matches($context, $reflector);
}

public function getKind(): int
Expand Down
6 changes: 2 additions & 4 deletions src/Aop/Pointcut/OrPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,10 @@ public function __construct(Pointcut ...$pointcuts)

public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
null|object|string $instanceOrScope = null,
?array $arguments = null
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null
): bool {
foreach ($this->pointcuts as $singlePointcut) {
if ($singlePointcut->matches($context, $reflector, $instanceOrScope, $arguments)) {
if ($singlePointcut->matches($context, $reflector)) {
return true;
}
}
Expand Down
Loading