diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf3e311..7511b967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Changelog * [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] `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. * [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. diff --git a/demos/Demo/Aspect/AwesomeAspectKernel.php b/demos/Demo/Aspect/AwesomeAspectKernel.php index 5c129faf..cfe49449 100644 --- a/demos/Demo/Aspect/AwesomeAspectKernel.php +++ b/demos/Demo/Aspect/AwesomeAspectKernel.php @@ -25,7 +25,6 @@ class AwesomeAspectKernel extends AspectKernel */ protected function configureAop(AspectContainer $container): void { - $container->registerAspect(new DeclareErrorAspect()); $container->registerAspect(new CachingAspect()); $container->registerAspect(new LoggingAspect()); $container->registerAspect(new IntroductionAspect()); diff --git a/demos/Demo/Aspect/DeclareErrorAspect.php b/demos/Demo/Aspect/DeclareErrorAspect.php deleted file mode 100644 index 1930b488..00000000 --- a/demos/Demo/Aspect/DeclareErrorAspect.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Demo\Aspect; - -use Go\Aop\Aspect; -use Go\Lang\Attribute\DeclareError; - -/** - * This aspect can be very useful for development to generate an error when executing prohibited methods - */ -class DeclareErrorAspect implements Aspect -{ - /** - * Prevent developers from using this method by always generating a warning - */ - #[DeclareError('execution(public Demo\Example\ErrorDemo->notSoGoodMethod(*))', level: E_USER_WARNING)] - protected string $badMethod = 'Method can generate division by zero! Do not use it!'; -} diff --git a/demos/Demo/Example/ErrorDemo.php b/demos/Demo/Example/ErrorDemo.php deleted file mode 100644 index a4b3e62b..00000000 --- a/demos/Demo/Example/ErrorDemo.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Demo\Example; - -use Deprecated; - -/** - * In this class we use system functions that will be intercepted by aspect - */ -class ErrorDemo -{ - /** - * Some old method that is used by system in production, but shouldn't be used for the new code - */ - #[Deprecated] - public function oldMethod(): void - { - echo "Hello, I'm old method", PHP_EOL; - } - - /** - * Method that is very tricky and should generate a notice - */ - public function notSoGoodMethod(): float - { - $value = round(microtime(true)) % 3; // Sometimes this equal to 0 - - return rand(1, 10) / $value; - } -} diff --git a/demos/index.php b/demos/index.php index bde7129c..ee1ae71b 100644 --- a/demos/index.php +++ b/demos/index.php @@ -52,7 +52,6 @@
  • Fluent Interface
  • Human live advices
  • Dynamic traits and interfaces
  • -
  • Declare runtime errors
  • Documentation
  • @@ -120,7 +119,6 @@ use Demo\Example\CacheableDemo; use Demo\Example\DynamicMethodsDemo; -use Demo\Example\ErrorDemo; use Demo\Example\FunctionDemo; use Demo\Example\HumanDemo; use Demo\Example\IntroductionDemo; @@ -217,14 +215,6 @@ $example->testStringable(); break; - case 'declare-errors': - $aspectName = 'Demo\Aspect\DeclareErrorAspect'; - - $example = new ErrorDemo(); - $example->oldMethod(); - $example->notSoGoodMethod(); - break; - default: } ?> diff --git a/src/Aop/AGENTS.md b/src/Aop/AGENTS.md index b58e5502..9caf9c28 100644 --- a/src/Aop/AGENTS.md +++ b/src/Aop/AGENTS.md @@ -42,7 +42,7 @@ Proxy generators use TypeGenerator::renderTypeForPhpDoc() to emit V as 2nd gener ## Attributes (src/Lang/Attribute/) - Advice: #[Before], #[After], #[Around], #[AfterThrowing] -- Declaration: #[Aspect], #[Pointcut], #[DeclareError], #[DeclareParents] +- Declaration: #[Aspect], #[Pointcut], #[DeclareParents] - Base: AbstractAttribute, AbstractInterceptor, Interceptor (interface) ## Features (src/Aop/Features.php) diff --git a/src/Aop/Framework/DeclareErrorInterceptor.php b/src/Aop/Framework/DeclareErrorInterceptor.php deleted file mode 100644 index a311480a..00000000 --- a/src/Aop/Framework/DeclareErrorInterceptor.php +++ /dev/null @@ -1,98 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Framework; - -use Closure; -use Go\Aop\Intercept\Interceptor; -use Go\Aop\Intercept\Joinpoint; -use Stringable; - -/** - * Interceptor to dynamically trigger a user notice/warning/error on method call - * - * This interceptor can be used as active replacement for the "deprecated" tag or to notify about - * probable issues with specific method. - * - * @phpstan-type InterceptorState array{message: non-empty-string, level: positive-int, pointcutExpression: string} - */ -final readonly class DeclareErrorInterceptor implements Interceptor -{ - private Closure $adviceMethod; - - /** - * Default constructor for interceptor - * - * @param non-empty-string $message Error message to show for this interceptor - * @param positive-int $level Default level of error, only E_USER_* constants - * @param string $pointcutExpression Pointcut expression used - */ - public function __construct( - private string $message, - private int $level, - private string $pointcutExpression - ) { - $this->adviceMethod = self::declareErrorAdvice(...); - } - - public function invoke(Joinpoint $joinpoint): mixed - { - ($this->adviceMethod)($joinpoint, $this->message, $this->level); - - return $joinpoint->proceed(); - } - - /** - * Serializes an interceptor into its array shape representation - * - * @phpstan-return InterceptorState - */ - final public function __serialize(): array - { - return [ - 'message' => $this->message, - 'level' => $this->level, - 'pointcutExpression' => $this->pointcutExpression, - ]; - } - - - /** - * Un-serializes an interceptor from its stored state - * - * @phpstan-param InterceptorState $state The stored representation of the interceptor. - */ - final public function __unserialize(array $state): void - { - [ - 'message' => $this->message, - 'level' => $this->level, - 'pointcutExpression' => $this->pointcutExpression - ] = $state; - $this->adviceMethod = self::declareErrorAdvice(...); - } - - /** - * Returns an advice - */ - private static function declareErrorAdvice(Stringable $joinPoint, string $message, int $level): void - { - $message = vsprintf( - '[AOP Declare Error]: %s has an error: "%s"', - [ - (string) $joinPoint, - $message - ] - ); - trigger_error($message, $level); - } -} diff --git a/src/Aop/Pointcut/AttributePointcut.php b/src/Aop/Pointcut/AttributePointcut.php index ef99d56b..a9d23c51 100644 --- a/src/Aop/Pointcut/AttributePointcut.php +++ b/src/Aop/Pointcut/AttributePointcut.php @@ -59,7 +59,7 @@ final public function matches( $instanceToCheck = $reflector; } - if (!isset($instanceToCheck) || $instanceToCheck instanceof ReflectionFileNamespace) { + if ($instanceToCheck instanceof ReflectionFileNamespace) { return false; } diff --git a/src/Aop/Support/PointcutBuilder.php b/src/Aop/Support/PointcutBuilder.php index c7a6260c..937086d6 100644 --- a/src/Aop/Support/PointcutBuilder.php +++ b/src/Aop/Support/PointcutBuilder.php @@ -18,7 +18,6 @@ use Go\Aop\Framework\AfterThrowingInterceptor; use Go\Aop\Framework\AroundInterceptor; use Go\Aop\Framework\BeforeInterceptor; -use Go\Aop\Framework\DeclareErrorInterceptor; use Go\Core\AspectContainer; /** @@ -72,18 +71,6 @@ public function around(string $pointcutExpression, Closure $adviceToInvoke): voi $this->registerAdviceInContainer($pointcutExpression, $interceptor); } - /** - * Declares the error message for specific pointcut expression with concrete error level - * - * @param non-empty-string $message Error message to show for this intercepton - * @param positive-int $errorLevel Default level of error, only E_USER_* constants - */ - public function declareError(string $pointcutExpression, string $message, int $errorLevel = E_USER_ERROR): void - { - $interceptor = new DeclareErrorInterceptor($message, $errorLevel, $pointcutExpression); - $this->registerAdviceInContainer($pointcutExpression, $interceptor); - } - /** * General method to register advices */ diff --git a/src/Core/IntroductionAspectExtension.php b/src/Core/IntroductionAspectExtension.php index 8d8abae3..807fa190 100644 --- a/src/Core/IntroductionAspectExtension.php +++ b/src/Core/IntroductionAspectExtension.php @@ -14,12 +14,10 @@ use Go\Aop\Advice; use Go\Aop\Aspect; -use Go\Aop\Framework\DeclareErrorInterceptor; use Go\Aop\Framework\TraitIntroductionInfo; use Go\Aop\Pointcut; use Go\Aop\Support\GenericPointcutAdvisor; use Go\Lang\Attribute\AbstractAttribute; -use Go\Lang\Attribute\DeclareError; use Go\Lang\Attribute\DeclareParents; use ReflectionClass; use ReflectionProperty; @@ -51,11 +49,6 @@ public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array $advisor = new GenericPointcutAdvisor($pointcut, $advice); $loadedItems[$propertyId] = $advisor; - } elseif ($attribute instanceof DeclareError) { - $pointcut = $this->parsePointcut($aspect, $reflectionAspect, $attribute->expression); - $advice = $this->getAdvice($attribute, $aspect, $aspectProperty); - - $loadedItems[$propertyId] = new GenericPointcutAdvisor($pointcut, $advice); } else { throw new UnexpectedValueException('Unsupported attribute class: ' . get_class($attribute)); } @@ -76,27 +69,10 @@ protected function getAdvice( ReflectionProperty $aspectProperty ): Advice { return match (true) { - $interceptorAttribute instanceof DeclareError => - $this->createDeclareErrorAdvice($aspectProperty, $interceptorAttribute), $interceptorAttribute instanceof DeclareParents => new TraitIntroductionInfo($interceptorAttribute->traitName, $interceptorAttribute->interfaceName), default => throw new UnexpectedValueException('Unsupported attribute class: ' . get_class($interceptorAttribute)), }; } - - /** - * Creates a DeclareErrorInterceptor after validating the property's default value. - * - * @throws \UnexpectedValueException if the property default value is not a non-empty string - */ - private function createDeclareErrorAdvice(ReflectionProperty $aspectProperty, DeclareError $attribute): DeclareErrorInterceptor - { - $errorMessage = $aspectProperty->getDefaultValue(); - if (!is_string($errorMessage) || $errorMessage === '') { - throw new \UnexpectedValueException('DeclareError property must have a non-empty string default value'); - } - - return new DeclareErrorInterceptor($errorMessage, $attribute->level, $attribute->expression); - } } diff --git a/src/Lang/Attribute/DeclareError.php b/src/Lang/Attribute/DeclareError.php deleted file mode 100644 index 6666fa9c..00000000 --- a/src/Lang/Attribute/DeclareError.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Lang\Attribute; - -use Attribute; - -/** - * Declares error attribute - */ -#[Attribute(Attribute::TARGET_PROPERTY)] -class DeclareError extends AbstractAttribute -{ - /** - * @inheritdoc - * @param int&(\E_USER_NOTICE|\E_USER_WARNING|\E_USER_ERROR|\E_USER_DEPRECATED) $level Default level of error, only E_USER_* constants - */ - public function __construct( - string $expression, - readonly public int $level = E_USER_NOTICE, - int $order = 0, - ) { - parent::__construct($expression, $order); - } -} diff --git a/tests/Aop/Framework/DeclareErrorInterceptorTest.php b/tests/Aop/Framework/DeclareErrorInterceptorTest.php deleted file mode 100644 index 066266e8..00000000 --- a/tests/Aop/Framework/DeclareErrorInterceptorTest.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Framework; - -use Go\Aop\Intercept\Joinpoint; -use PHPUnit\Framework\TestCase; - -class DeclareErrorInterceptorTest extends TestCase -{ - public function testCanSerializeAndUnserialize(): void - { - $interceptor = new DeclareErrorInterceptor('Deprecated method', E_USER_DEPRECATED, 'execution(Some::method)'); - - $serialized = serialize($interceptor); - $unserialized = unserialize($serialized); - - $this->assertInstanceOf(DeclareErrorInterceptor::class, $unserialized); - } - - public function testUnserializedInterceptorPreservesMessageAndLevel(): void - { - $interceptor = new DeclareErrorInterceptor('Deprecated method', E_USER_DEPRECATED, 'execution(Some::method)'); - - /** @var DeclareErrorInterceptor $unserialized */ - $unserialized = unserialize(serialize($interceptor)); - - $joinpoint = $this->createMock(Joinpoint::class); - $joinpoint->expects($this->once())->method('proceed')->willReturn(null); - - $capturedMessage = null; - $capturedLevel = null; - set_error_handler(function (int $level, string $message) use (&$capturedMessage, &$capturedLevel): bool { - $capturedLevel = $level; - $capturedMessage = $message; - return true; - }); - $unserialized->invoke($joinpoint); - restore_error_handler(); - - $this->assertSame(E_USER_DEPRECATED, $capturedLevel); - $this->assertStringContainsString('Deprecated method', $capturedMessage); - } -}