.gitignore000064400000000114144760133060006534 0ustar00vendor/ composer.lock phpunit.xml tests/Fixtures/project/var/cache/* build/ .scrutinizer.yml000064400000001506144760133060007734 0ustar00before_commands: - "composer install --no-dev --prefer-source" tools: external_code_coverage: timeout: 1200 php_code_coverage: enabled: true php_code_sniffer: enabled: true config: standard: PSR2 filter: paths: ["src/*"] php_cpd: enabled: true excluded_dirs: ["docs", "demos", "tests", "vendor"] php_cs_fixer: enabled: true config: level: all filter: paths: ["src/*"] php_loc: enabled: true excluded_dirs: ["docs", "demos", "tests", "vendor"] php_pdepend: enabled: true excluded_dirs: ["docs", "demos", "tests", "vendor"] php_analyzer: enabled: true filter: paths: ["src/*"] sensiolabs_security_checker: true .travis.coverage.sh000075500000000260144760133060010265 0ustar00set -x if [ "$TRAVIS_PHP_VERSION" = '7.2' ] ; then wget https://scrutinizer-ci.com/ocular.phar php ocular.phar code-coverage:upload --format=php-clover ./clover.xml fi .travis.yml000064400000002241144760133060006660 0ustar00matrix: include: - language: php php: 7.2 os: linux before_script: - composer install - language: php php: 7.3 os: linux before_script: - composer install after_script: - sh .travis.coverage.sh env: - COVERAGE_FLAGS="--coverage-text --coverage-clover=clover.xml" - language: php php: 7.4 before_script: - composer install - language: php php: nightly before_script: - composer install - name: "PHP: 7.2" language: sh os: windows before_script: - choco install php --version 7.3.12 --package-parameters='"/InstallDir:C:\php"' # Export windows path into unix path - export PATH=/c/php:$PATH # Enable openssl, mbstring php extensions - sed -i 's/;extension=openssl/extension=openssl/g' /c/php/php.ini - sed -i 's/;extension=mbstring/extension=mbstring/g' /c/php/php.ini - wget http://getcomposer.org/composer.phar - php composer.phar install allow_failures: - php: nightly - php: 7.4 script: - vendor/bin/phpunit --verbose ${COVERAGE_FLAGS} --colors CHANGELOG.md000064400000013162144760133060006364 0ustar00Changelog ====== 3.0.0 (December 4, 2019) * [BC BREAK] Switched to the PHP7.2 and upper, strict types, return type hints and new syntax * [BC BREAK] Removed the Joinpoint->getThis() method, as not all joinpoints belongs to classes (eg. FunctionInvocation) * [BC BREAK] Removed the Joinpoint->getStaticPart() method as it can return anything, better to use explicit methods * [Feature] Introduced the new ClassJoinpoint interface with getScope(), getThis() and isDynamic() methods * [Feature] Implemented parameter widening feature for generated code #380 * [Feature] AnnotatedReflectionProperty provides simple access to property annotations #388 by @TheCelavi * [Feature] Switched to the `zendframework/zend-code` package to generate code for proxies * [Feature] Add private properties interception #412 2.0.0 (May 14, 2016) * Dropped support for PHP<5.6, clean all old code * [BC BREAK] Removed ability to rebind closures, because of PHP restrictions, see #247 * [BC BREAK] Removed getDefaultFeatures() method from the AspectKernel, no need in it since PHP5.6 * Migrated from the `Andrewswille/Token-Reflection` to the `goaop/parser-reflection` library for PHP5.6 and PHP7.0 support * Added support for PHP5.6 and 7.0 features: variadic methods, scalar type hints, return type hints * [Feature] Command-line tools for debugging aspects and advisors 1.0.0 (Feb 13, 2016) * Dropped support for PHP<5.5, clean all old code * Tagged public methods and interfaces with @api tag. No more changes for them in future. * Refactored core code to use general interceptors for everything instead of separate classes * New static initialization pointcut to intercept the moment of class loading * New feature to intercept object initializations, requires INTERCEPT_INITIALIZATIONS to be enabled * [BC BREAK] remove class() pointcut from the grammar #189 * [BC BREAK] make within() and @within() match all joinpoints #189 * [BC BREAK] drop @annotation syntax. Add @execution pointcut * Pointcuts can be build now directly from closures via `PointcutBuilder` class * Do not create files in the cache, if no aspects were applied to them, respects `includePath` option now * `FilterInjector` is now disabled by default, this job for composer integration now * Automatic opcache invalidation for cache state file 0.6.1 (Jul 5, 2015) * Minor patch to fix a bug with overwriting files 0.6.0 (Feb 1, 2015) * Interceptor for magic methods via "dynamic" pointcut. This feature also gives an access for dynamic pointcuts with different checks and conditions. * PSR-4 standard for the codebase, thanks to @cordoval * Added a support for splat (...) operator for more efficient advice invocation (requires PHP5.6) * New feature system. All tunings of kernel are configured with feature-set. This breaks old configuration option `interceptFunctions=>true` use `'features' => $defaultFeatures | Features::INTERCEPT_FUNCTIONS` now * Proxy can generate more effective invocation call with `static::class` for PHP>=5.5 * Bug-fixes with empty cache path and PSR4 code, thanks to @andy-shea * Make pointcut grammar class compatible with PHP7.0 0.5.0 (May 24, 2014) * Proxies are now stored in the separate files to allow more transparent debugging * Cache warmer command added * Extended pointcut syntax for or-ed methods: ClassName->method1|method2(*) * Access to the annotations for method from MethodInvocation * Support for read-only file systems (phar, GAE, etc) * Direct access to advisors (no more serialize/unserialize) * New @within pointcut to match classes by annotation class * Nice demo GUI * Deprecate the usage of submodules for framework * Inheritance support during class-loading and weaving * List of small fixes and imrovements 0.4.1 (Aug 27, 2013) * Better parsing of complex "include" expressions for Yii (by @zvirusz) * Support for dynamic arguments count for methods by checking for func_get_args() inside method body * Fixed a bug with autoloaders reodering (by @zvirusz) 0.4.0 (Aug 04, 2013) * Privileged advices for aspect: allows to access private and protected properties and methods of objects inside advice * Full integration with composer that allows for easy configuration and workflow with AOP * Fix some bugs with caching on Windows * "True" pointcut references that gives the ability to compose a complex pointcut from a simple pointcuts. * Pointcut now accept "$this" in references to point to the current aspect instance (Allows for abstract aspects and abstract pointcuts) * AspectContainer interface was extracted. This gives the way to integrate with another DIC. Look at Warlock framework. * Intercepting system functions such as `fopen()`, `file_get_contents()`, etc * Annotation property pointcut was added * Ability to declare multiple interfaces and/or traits with single `DeclareParent` introduction * DeclareError interceptor was added. This can be used for generating an runtime error for methods that should not be executed in such a way. 0.3.0 (May 27, 2013) * Support for dynamic pointcuts: pointcut that match a specific point in the code, if it is under the control flow (look at AspectJ cflow and cflowbelow) * Performance optimizations * Case-sensitive matching for pointcuts * Primitive pointcuts (&&, ||, !) * [BC break] Changes in the kernel configuration (look at the demo for appLoader and autoloadPaths) * Fix a logic bug for a composite pointcuts 0.2.0 (Mar 15, 2013) * Intercepting methods in traits * Pointcut parser/grammar * Huge pointcuts refactoring, cleaning * Lazy loading services, pointcuts 0.1.1 (Jan 20, 2013) * Introduction advice support * Fix bug with composer autoloader prepending * Fix doctrine/common dependency: >=2.0.0, <2.4.0 0.1.0 (Jan 08, 2013) * Initial release of library CONTRIBUTE.md000064400000001316144760133060006551 0ustar00# How to contribute ## Installation Before you contribute make sure you install all necessary dependencies using: ```bash $ composer install ``` ## Run Tests To run tests simply run the `phpunit` executable in the `vendor/bin` ```bash $ ./vendor/bin/phpunit ``` You should get an output similar to this ```bash $ ./vendor/bin/phpunit PHPUnit 4.8.29 by Sebastian Bergmann and contributors. ..........................SSSSSS............................... 63 / 157 ( 40%) ............................................................... 126 / 157 ( 80%) ............................... Time: 658 ms, Memory: 25.00MB OK, but incomplete, skipped, or risky tests! Tests: 157, Assertions: 207, Skipped: 6. ``` LICENSE000064400000002102144760133060005550 0ustar00Copyright (c) 2019 Lisachenko Alexander Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Procfile000064400000000050144760133060006231 0ustar00web: vendor/bin/heroku-php-nginx demos/ README.md000064400000024070144760133060006032 0ustar00Go! Aspect-Oriented Framework for PHP ----------------- Go! AOP is a modern aspect-oriented framework in plain PHP with rich features for the new level of software development. The framework allows cross-cutting issues to be solved in the traditional object-oriented PHP code by providing a highly efficient and transparent hook system for your exisiting code. [![Build Status](https://secure.travis-ci.org/goaop/framework.png?branch=master)](https://travis-ci.org/goaop/framework) [![GitHub release](https://img.shields.io/github/release/goaop/framework.svg)](https://github.com/goaop/framework/releases/latest) [![Total Downloads](https://img.shields.io/packagist/dt/goaop/framework.svg)](https://packagist.org/packages/goaop/framework) [![Daily Downloads](https://img.shields.io/packagist/dd/goaop/framework.svg)](https://packagist.org/packages/goaop/framework) [![SensioLabs Insight](https://img.shields.io/sensiolabs/i/9f3e6de1-ea14-4910-b2de-99ff431c9252.svg)](https://insight.sensiolabs.com/projects/9f3e6de1-ea14-4910-b2de-99ff431c9252) [![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%207.2-8892BF.svg)](https://php.net/) [![License](https://img.shields.io/packagist/l/goaop/framework.svg)](https://packagist.org/packages/goaop/framework) Features ------------ * Provides dynamic hook system for PHP without changes in the original source code. * Doesn't require any PECL-extentions (php-aop, runkit, uopz) and DI-containers to work. * Object-oriented design of aspects, joinpoints and pointcuts. * Intercepting an execution of any public or protected method in a classes. * Intercepting an execution of static methods and methods in final classes. * Intercepting an execution of methods in the traits. * Intercepting an access to the public/protected properties for objects. * Hooks for static class initialization (after class is loaded into PHP memory). * Hooks for object initialization (intercepting `new` keywords). * Intercepting an invocation of system PHP functions. * Ability to change the return value of any methods/functions via `Around` type of advice. * Rich pointcut grammar syntax for defining pointcuts in the source code. * Native debugging for AOP with XDebug. The code with weaved aspects is fully readable and native. You can put a breakpoint in the original class or in the aspect and it will work (for debug mode)! * Can be integrated with any existing PHP frameworks and libraries (with or without additional configuration). * Highly optimized for production use: support of opcode cachers, lazy loading of advices and aspects, joinpoints caching, no runtime checks of pointcuts, no runtime annotations parsing, no evals and `__call` methods, no slow proxies and `call_user_func_array()`. Fast bootstraping process (2-20ms) and advice invocation. What is AOP? ------------ [AOP (Aspect-Oriented Programming)](http://en.wikipedia.org/wiki/Aspect-oriented_programming) is an approach to cross-cutting concerns, where these concerns are designed and implemented in a "modular" way (that is, with appropriate encapsulation, lack of duplication, etc.), then integrated into all the relevant execution points in a succinct and robust way, e.g. through declarative or programmatic means. In AOP terms, the execution points are called join points. A set of those points is called a pointcut and the new behavior that is executed before, after, or "around" a join point is called advice. You can read more about AOP in [Introduction](http://go.aopphp.com/docs/introduction/) section. Installation ------------ Go! AOP framework can be installed with composer. Installation is quite easy: 1. Download the framework using composer 2. Create an application aspect kernel 3. Configure the aspect kernel in the front controller 4. Create an aspect 5. Register the aspect in the aspect kernel ### Step 0 (optional): Try demo examples in the framework Ask composer to create new project in empty directory: ```bash composer create-project goaop/framework ``` After that just configure your web server to `demos/` folder and open it in your browser. Then you can look at some demo examples before going deeper into installing it in your project. ### Step 1: Download the library using composer Ask composer to download the latest version of Go! AOP framework with its dependencies by running the command: ``` bash composer require goaop/framework ``` Composer will install the framework to your project's `vendor/goaop/framework` directory. ### Step 2: Create an application aspect kernel The aim of this framework is to provide easy AOP integration for your application. You have to first create the `AspectKernel` class for your application. This class will manage all aspects of your application in one place. The framework provides base class to make it easier to create your own kernel. To create your application kernel, extend the abstract class `Go\Core\AspectKernel` ``` php init([ 'debug' => true, // use 'false' for production mode 'appDir' => __DIR__ . '/..', // Application root directory 'cacheDir' => __DIR__ . '/path/to/cache/for/aop', // Cache directory // Include paths restricts the directories where aspects should be applied, or empty for all source files 'includePaths' => [ __DIR__ . '/../src/' ] ]); ``` ### 4. Create an aspect Aspect is the key element of AOP philosophy. Go! AOP framework just uses simple PHP classes for declaring aspects, which makes it possible to use all features of OOP for aspect classes. As an example let's intercept all the methods and display their names: ``` php // Aspect/MonitorAspect.php namespace Aspect; use Go\Aop\Aspect; use Go\Aop\Intercept\FieldAccess; use Go\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\After; use Go\Lang\Annotation\Before; use Go\Lang\Annotation\Around; use Go\Lang\Annotation\Pointcut; /** * Monitor aspect */ class MonitorAspect implements Aspect { /** * Method that will be called before real method * * @param MethodInvocation $invocation Invocation * @Before("execution(public Example->*(*))") */ public function beforeMethodExecution(MethodInvocation $invocation) { echo 'Calling Before Interceptor for: ', $invocation, ' with arguments: ', json_encode($invocation->getArguments()), "
\n"; } } ``` Easy, isn't it? We declared here that we want to install a hook before the execution of all dynamic public methods in the class Example. This is done with the help of annotation `@Before("execution(public Example->*(*))")` Hooks can be of any types, you will see them later. But we don't change any code in the class Example! I can feel your astonishment now. ### 5. Register the aspect in the aspect kernel To register the aspect just add an instance of it in the `configureAop()` method of the kernel: ``` php // app/ApplicationAspectKernel.php use Aspect\MonitorAspect; //... protected function configureAop(AspectContainer $container) { $container->registerAspect(new MonitorAspect()); } //... ``` ### 6. Optional configurations #### 6.1 Custom annotation cache By default, Go! AOP uses `Doctrine\Common\Cache\FilesystemCache` for caching annotations. However, if you need to use any other caching engine for annotation, you may configure cache driver via `annotationCache` configuration option of your application aspect kernel. Only requirement is that cache driver implements `Doctrine\Common\Cache\Cache` interface. This can be very useful when deploying to read-only filesystems. In that case, you may use, per example, `Doctrine\Common\Cache\ArrayCache` or some memory-based cache driver. #### 6.2 Support for weaving Doctrine entities (experimental, alpha) Weaving Doctrine entities can not be supported out of the box due to the fact that Go! AOP generates two sets of classes for each weaved entity, a concrete class and proxy with pointcuts. Doctrine will interpret both of those classes as concrete entities and assign for both of them same metadata, which would mess up the database and relations (see [https://github.com/goaop/framework/issues/327](https://github.com/goaop/framework/issues/327)). Therefore, a workaround is provided with this library which will sort out mapping issue in Doctrine. Workaround is in form of event subscriber, `Go\Bridge\Doctrine\MetadataLoadInterceptor` which has to be registered when Doctrine is bootstraped in your project. For details how to do that, see [http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html). Event subscriber will modify metadata entity definition for generated Go! Aop proxies as mapped superclass. That would sort out issues on which you may stumble upon when weaving Doctrine entities. ### 7. Contribution To contribute changes see the [Contribute Readme](CONTRIBUTE.md) Documentation ------------- Documentation about Go! library can be found at [official site][1]. If you like this project, you could support it via Flattr this [1]: http://go.aopphp.com bin/aspect000075500000002566144760133060006536 0ustar00#!/usr/bin/env php * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ use Go\Console\Command\DebugAdvisorCommand; use Go\Console\Command\DebugAspectCommand; use Go\Console\Command\CacheWarmupCommand; use Go\Console\Command\DebugWeavingCommand; use Go\Core\AspectKernel; use Symfony\Component\Console\Application; if (is_dir($vendor = __DIR__ . '/../vendor')) { require $vendor . '/autoload.php'; } elseif (is_dir($vendor = __DIR__ . '/../../..')) { require $vendor . '/autoload.php'; } else { die( 'You must set up the project dependencies.' . PHP_EOL . 'To do that, run the following commands:' . PHP_EOL . PHP_EOL . '$ curl -s http://getcomposer.org/installer | php' . PHP_EOL . '$ php composer.phar install' . PHP_EOL ); } if (!class_exists('Symfony\Component\Console\Application')) { die( 'You must install the symfony/console package in order ' . 'to use the command-line tool.' . PHP_EOL ); } $app = new Application('Go! AOP', AspectKernel::VERSION); $app->add(new CacheWarmupCommand()); $app->add(new DebugAspectCommand()); $app->add(new DebugAdvisorCommand()); $app->add(new DebugWeavingCommand()); $app->run(); composer.json000064400000003056144760133060007276 0ustar00{ "name": "goaop/framework", "description": "Framework for aspect-oriented programming in PHP.", "type": "library", "keywords": ["library", "aop", "php", "aspect"], "homepage": "http://go.aopphp.com/", "license": "MIT", "require": { "php": ">=7.2.0", "jakubledl/dissect": "~1.0", "doctrine/annotations": "^1.2.3", "goaop/parser-reflection": "~2.0", "doctrine/cache": "^1.5", "zendframework/zend-code": "^3.1", "symfony/finder": "^3.4|^4.2|^5.0" }, "require-dev": { "symfony/console": "^2.7|^3.0", "adlawson/vfs": "^0.12", "doctrine/orm": "^2.5", "symfony/filesystem": "^3.3", "symfony/process": "^3.3", "phpunit/phpunit": "^7.5", "webmozart/glob": "^4.1" }, "suggest": { "symfony/console": "Enables the usage of the command-line tool." }, "bin": ["bin/aspect"], "authors": [ { "name": "Lisachenko Alexander", "homepage": "https://github.com/lisachenko" } ], "autoload": { "psr-4": { "Go\\": "src/" } }, "autoload-dev": { "psr-0": { "Demo\\": "demos/" }, "psr-4": { "Go\\": "tests/Go/", "Go\\Tests\\TestProject\\": "tests/Fixtures/project/src/" }, "files": [ "tests/functions.php" ] }, "minimum-stability": "stable", "extra": { "branch-alias": { "dev-master": "3.0-dev" } } } demos/Demo/Annotation/Cacheable.php000064400000000753144760133060013222 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Annotation; use Doctrine\Common\Annotations\Annotation; /** * @Annotation * @Target("METHOD") */ class Cacheable extends Annotation { /** * Time to cache * * @var int */ public $time = 0; } demos/Demo/Annotation/Deprecated.php000064400000000633144760133060013430 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Annotation; use Doctrine\Common\Annotations\Annotation; /** * @Annotation * @Target("METHOD") */ class Deprecated extends Annotation { } demos/Demo/Annotation/Loggable.php000064400000000631144760133060013102 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Annotation; use Doctrine\Common\Annotations\Annotation; /** * @Annotation * @Target("METHOD") */ class Loggable extends Annotation { } demos/Demo/Aspect/AwesomeAspectKernel.php000064400000002262144760133060014376 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Aspect; use Go\Core\AspectKernel; use Go\Core\AspectContainer; /** * Awesome Aspect Kernel class */ class AwesomeAspectKernel extends AspectKernel { /** * Configure an AspectContainer with advisors, aspects and pointcuts * * @param AspectContainer $container */ protected function configureAop(AspectContainer $container) { $container->registerAspect(new DeclareErrorAspect()); $container->registerAspect(new CachingAspect()); $container->registerAspect(new LoggingAspect()); $container->registerAspect(new IntroductionAspect()); $container->registerAspect(new PropertyInterceptorAspect()); $container->registerAspect(new FunctionInterceptorAspect()); $container->registerAspect(new FluentInterfaceAspect()); $container->registerAspect(new HealthyLiveAspect()); $container->registerAspect(new DynamicMethodsAspect()); } } demos/Demo/Aspect/CachingAspect.php000064400000003333144760133060013171 0ustar00 * * 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\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\Around; /** * Caching aspect */ class CachingAspect implements Aspect { /** * This advice intercepts an execution of cacheable methods * * Logic is pretty simple: we look for the value in the cache and if it's not present here * then invoke original method and store it's result in the cache. * * Real-life examples will use APC or Memcache to store value in the cache * * @param MethodInvocation $invocation Invocation * * @Around("@execution(Demo\Annotation\Cacheable)") */ public function aroundCacheable(MethodInvocation $invocation) { static $memoryCache = []; $time = microtime(true); $obj = $invocation->getThis(); $class = is_object($obj) ? get_class($obj) : $obj; $key = $class . ':' . $invocation->getMethod()->name; if (!isset($memoryCache[$key])) { // We can use ttl value from annotation, but Doctrine annotations doesn't work under GAE if (!isset($_SERVER['APPENGINE_RUNTIME'])) { echo "Ttl is: ", $invocation->getMethod()->getAnnotation('Demo\Annotation\Cacheable')->time, PHP_EOL; } $memoryCache[$key] = $invocation->proceed(); } echo "Take ", sprintf("%0.3f", (microtime(true) - $time) * 1e3), "ms to call method $key", PHP_EOL; return $memoryCache[$key]; } } demos/Demo/Aspect/DeclareErrorAspect.php000064400000002100144760133060014175 0ustar00 * * 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\Annotation\DeclareError; /** * This aspect can be very useful for development to generate an error when executing prohibited methods */ class DeclareErrorAspect implements Aspect { /** * Message to show when calling the method * * @var string * @DeclareError("@execution(Demo\Annotation\Deprecated)", level=16384) // E_USER_DEPRECATED */ protected $message = 'Method is deprecated and should not be called in debug mode'; /** * Prevent developers from using this method by always generating a warning * * @var string * @DeclareError("execution(public Demo\Example\ErrorDemo->notSoGoodMethod(*))", level=512) // E_USER_WARNING */ protected $badMethod = 'Method can generate division by zero! Do not use it!'; } demos/Demo/Aspect/DynamicMethodsAspect.php000064400000003764144760133060014555 0ustar00 * * 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\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\Before; /** * Aspect that intercepts specific magic methods, declared with __call and __callStatic */ class DynamicMethodsAspect implements Aspect { /** * This advice intercepts an execution of __call methods * * Unlike traditional "execution" pointcut, "dynamic" is checking the name of method in * the runtime, allowing to write interceptors for __call more transparently. * * @param MethodInvocation $invocation Invocation * * @Before("dynamic(public Demo\Example\DynamicMethodsDemo->save*(*))") */ public function beforeMagicMethodExecution(MethodInvocation $invocation) { // we need to unpack args from invocation args list($methodName, $args) = $invocation->getArguments(); echo 'Calling Magic Interceptor for method: ', $invocation->getScope(), '->', $methodName, '()', ' with arguments: ', json_encode($args), PHP_EOL; } /** * This advice intercepts an execution of methods via __callStatic * * @param MethodInvocation $invocation * @Before("dynamic(public Demo\Example\DynamicMethodsDemo::find*(*))") */ public function beforeMagicStaticMethodExecution(MethodInvocation $invocation) { // we need to unpack args from invocation args list($methodName, $args) = $invocation->getArguments(); echo 'Calling Static Magic Interceptor for method: ', $invocation->getScope(), '::', $methodName, '()', ' with arguments: ', json_encode($args), PHP_EOL; } } demos/Demo/Aspect/FluentInterface.php000064400000000527144760133060013555 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Aspect; /** * Fluent interface marker */ interface FluentInterface { } demos/Demo/Aspect/FluentInterfaceAspect.php000064400000002377144760133060014722 0ustar00 * * 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\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\Around; /** * Fluent interface aspect provides an easy way to reuse "chain" interface for classes * * Basically, it uses around method to intercept all public setters in the class that implements * special marker interface FluentInterface. Then it checks the return value for setter, if it's null, * then advice replaces it with reference to the object "$this". * * @see http://go.aopphp.com/blog/2013/03/19/implementing-fluent-interface-pattern-in-php/ */ class FluentInterfaceAspect implements Aspect { /** * Fluent interface advice * * @Around("within(Demo\Aspect\FluentInterface+) && execution(public **->set*(*))") * * @param MethodInvocation $invocation * @return mixed|null|object */ protected function aroundMethodExecution(MethodInvocation $invocation) { $result = $invocation->proceed(); return $result ?? $invocation->getThis(); } } demos/Demo/Aspect/FunctionInterceptorAspect.php000064400000003267144760133060015647 0ustar00 * * 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\Aop\Intercept\FunctionInvocation; use Go\Lang\Annotation\Around; /** * Function interceptor can intercept an access to the system functions */ class FunctionInterceptorAspect implements Aspect { /** * This advice intercepts an access to the array_*** function in Demo\Example\ namespace * * @param FunctionInvocation $invocation * * @Around("execution(Demo\Example\array_*(*))") * * @return mixed */ public function aroundArrayFunctions(FunctionInvocation $invocation) { echo 'Calling Around Interceptor for ', $invocation, ' with arguments: ', json_encode($invocation->getArguments()), PHP_EOL; return $invocation->proceed(); } /** * This advice intercepts an access to the file_get_contents() function * * @param FunctionInvocation $invocation * * @Around("execution(Demo\Example\file_get_contents(*))") * * @return mixed */ public function aroundFileGetContents(FunctionInvocation $invocation) { echo 'Calling Around Interceptor for ', $invocation, ' with arguments: ', json_encode($invocation->getArguments()), PHP_EOL; // return $invocation->proceed(); // Do not call original file_get_contents() return 'Hello!'; // Override return value for function } } demos/Demo/Aspect/HealthyLiveAspect.php000064400000003435144760133060014056 0ustar00 * * 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\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\After; use Go\Lang\Annotation\Before; use Go\Lang\Annotation\Pointcut; /** * Healthy live aspect */ class HealthyLiveAspect implements Aspect { /** * Pointcut for eat method * * @Pointcut("execution(public Demo\Example\HumanDemo->eat(*))") */ protected function humanEat() {} /** * Washing hands before eating * * @param MethodInvocation $invocation Invocation * @Before("$this->humanEat") */ protected function washUpBeforeEat(MethodInvocation $invocation) { /** @var $person \Demo\Example\HumanDemo */ $person = $invocation->getThis(); $person->washUp(); } /** * Method that advices to clean the teeth after eating * * @param MethodInvocation $invocation Invocation * @After("$this->humanEat") */ protected function cleanTeethAfterEat(MethodInvocation $invocation) { /** @var $person \Demo\Example\HumanDemo */ $person = $invocation->getThis(); $person->cleanTeeth(); } /** * Method that advice to clean the teeth before going to sleep * * @param MethodInvocation $invocation Invocation * @Before("execution(public Demo\Example\HumanDemo->sleep(*))") */ protected function cleanTeethBeforeSleep(MethodInvocation $invocation) { /** @var $person \Demo\Example\HumanDemo */ $person = $invocation->getThis(); $person->cleanTeeth(); } } demos/Demo/Aspect/Introduce/SerializableImpl.php000064400000001674144760133060015667 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Aspect\Introduce; /** * Example class to test aspects */ trait SerializableImpl { /** * String representation of object * @return string the string representation of the object or null */ public function serialize() { return serialize(get_object_vars($this)); } /** * Constructs the object * @param string $serialized

* The string representation of the object. *

* @return mixed the original value unserialized. */ public function unserialize($serialized) { $data = unserialize($serialized); foreach ($data as $key=>$value) { $this->$key = $value; } } } demos/Demo/Aspect/IntroductionAspect.php000064400000001527144760133060014321 0ustar00 * * 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\Annotation\DeclareParents; /** * Introduction aspect can dynamically add new interfaces and traits to the class */ class IntroductionAspect implements Aspect { /** * Add a single interface and trait to the class. * * You can also give several interfaces/traits via [] * * @DeclareParents( * value="within(Demo\Example\IntroductionDemo)", * interface="Serializable", * defaultImpl="Demo\Aspect\Introduce\SerializableImpl" * ) * * @var null */ protected $introduction; } demos/Demo/Aspect/LoggingAspect.php000064400000002564144760133060013230 0ustar00 * * 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\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\Before; /** * Logging aspect * * @see http://go.aopphp.com/blog/2013/07/21/implementing-logging-aspect-with-doctrine-annotations/ */ class LoggingAspect implements Aspect { /** * This advice intercepts an execution of loggable methods * * We use "Before" type of advice to log only class name, method name and arguments before * method execution. * You can choose your own logger, for example, monolog or log4php. * Also you can choose "After" or "Around" advice to access an return value from method. * * To inject logger into this aspect you can look at Warlock framework with DI+AOP * * @param MethodInvocation $invocation Invocation * * @Before("@execution(Demo\Annotation\Loggable)") */ public function beforeMethodExecution(MethodInvocation $invocation) { echo 'Calling Before Interceptor for ', $invocation, ' with arguments: ', json_encode($invocation->getArguments()), PHP_EOL; } } demos/Demo/Aspect/PropertyInterceptorAspect.php000064400000002757144760133060015711 0ustar00 * * 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\Aop\Intercept\FieldAccess; use Go\Lang\Annotation\Around; /** * Property interceptor can intercept an access to the public and protected properties * * Be aware, it's very tricky and will not work for indirect modification, such as array_pop($this->property); */ class PropertyInterceptorAspect implements Aspect { /** * Advice that controls an access to the properties * * @param FieldAccess $fieldAccess Joinpoint * * @Around("access(public|protected|private Demo\Example\PropertyDemo->*)") * @return mixed */ public function aroundFieldAccess(FieldAccess $fieldAccess) { $isRead = $fieldAccess->getAccessType() == FieldAccess::READ; // proceed all internal advices $fieldAccess->proceed(); if ($isRead) { // if you want to change original property value, then return it by reference $value = /* & */$fieldAccess->getValue(); } else { // if you want to change value to set, then return it by reference $value = /* & */$fieldAccess->getValueToSet(); } echo "Calling After Interceptor for ", $fieldAccess, ", value: ", json_encode($value), PHP_EOL; } } demos/Demo/Example/CacheableDemo.php000064400000001501144760133060013300 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; use Demo\Annotation\Cacheable; /** * Example class to show how to use caching with AOP */ class CacheableDemo { /** * Returns a report and explicitly cache a result for future use * * In this example we use "Cacheable" annotation to explicit mark a method * * @param string $from This can be any value * @Cacheable(time=10) * * @return string */ public function getReport(string $from) : string { // long calculation for 100ms usleep(100 * 1000); return $from; } } demos/Demo/Example/DynamicMethodsDemo.php000064400000001776144760133060014377 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; /** * Example class to show how to intercept magic methods * * @method void saveById(int $id) * @method void saveByName(string $name) * @method void load(int $id) * @method static void find(array $args) */ class DynamicMethodsDemo { /** * Magic invoker * * @param string $name Method name * @param array $args Method arguments */ public function __call(string $name, array $args) { echo "I'm method: {$name}", PHP_EOL; } /** * Magic static invoker * * @param string $name Method name * @param array $args Method arguments */ public static function __callStatic(string $name, array $args) { echo "I'm static method: {$name}", PHP_EOL; } } demos/Demo/Example/ErrorDemo.php000064400000001675144760133060012556 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; use Demo\Annotation\Deprecated as deprecated; // <== We import specific system annotation /** * 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() { 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; } } demos/Demo/Example/FunctionDemo.php000064400000001477144760133060013252 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; /** * In this class we use system functions that will be intercepted by aspect */ class FunctionDemo { /** * Some array transformer * * @param array $data Incoming array * * @return array Outcoming array */ public function testArrayFunctions(array $data = []) { return array_flip(array_unique(array_values($data))); } /** * Outputs a file content */ public function testFileContent() { echo '
', htmlspecialchars(file_get_contents(__FILE__)), '
'; } } demos/Demo/Example/HumanDemo.php000064400000001612144760133060012524 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; /** * Human class example */ class HumanDemo { /** * Eat something */ public function eat() { echo "Eating...", PHP_EOL; } /** * Clean the teeth */ public function cleanTeeth() { echo "Cleaning teeth...", PHP_EOL; } /** * Washing up */ public function washUp() { echo "Washing up...", PHP_EOL; } /** * Working */ public function work() { echo "Working...", PHP_EOL; } /** * Go to sleep */ public function sleep() { echo "Go to sleep...", PHP_EOL; } } demos/Demo/Example/IntroductionDemo.php000064400000002316144760133060014137 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; /** * Example class to show how to dynamically add new interfaces and traits to the class */ class IntroductionDemo { /** * Method that checks if the current instance implementing Serializable interface */ public function testSerializable() { if ($this instanceof \Serializable) { echo get_class($this), ' implements `Serializable` interface now!', PHP_EOL; $reflection = new \ReflectionObject($this); echo "List of interfaces:", PHP_EOL; foreach ($reflection->getInterfaceNames() as $interfaceName) { echo '-> ', $interfaceName, PHP_EOL; } echo "List of traits:", PHP_EOL; foreach ($reflection->getTraitNames() as $traitName) { echo '-> ', $traitName, PHP_EOL; } } else { echo get_class($this), ' does not implement `Serializable` interface', PHP_EOL; } } } demos/Demo/Example/LoggingDemo.php000064400000002407144760133060013045 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; use Demo\Annotation\Loggable; /** * Example class to show how to use logging with AOP */ class LoggingDemo { /** * Executes a task and logs all incoming arguments * * @Loggable * @param string $task Some specific argument */ public function execute(string $task) { $this->perform($task, 'first'); $this->perform($task, 'second'); } /** * Protected method can be also loggable * * @Loggable * * @param string $task Specific task * @param string $level */ protected function perform(string $task, string $level) { // some logic here } /** * Everything is possible with AOP, so static methods can be intercepted too * * @Loggable * * @param string $task Some specific argument */ public static function runByName(string $task) { $instance = new static(); // Go! AOP requires LSB to work correctly $instance->execute($task); } } demos/Demo/Example/PropertyDemo.php000064400000002207144760133060013301 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; /** * Example class to show how to intercept an access to the properties */ class PropertyDemo { public $publicProperty = 123; protected $protectedProperty = 456; private $privateProperty = 'test'; protected $indirectModificationCheck = [4, 5, 6]; public function showProtected() { echo $this->protectedProperty, PHP_EOL; } public function setProtected($newValue) { $this->protectedProperty = $newValue; } public function showPrivate() { echo $this->privateProperty; } public function __construct() { array_push($this->indirectModificationCheck, 7, 8, 9); if (count($this->indirectModificationCheck) !== 6) { throw new \RuntimeException("Indirect modification doesn't work!"); } $this->privateProperty = $this->privateProperty . 'bar'; } } demos/Demo/Example/UserFluentDemo.php000064400000002036144760133060013551 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo\Example; use Demo\Aspect\FluentInterface; /** * Example class to show fluent interface in action */ class UserFluentDemo implements FluentInterface { protected $name; protected $surname; protected $password; public function setName(string $name) { echo "Set user name to ", $name, PHP_EOL; $this->name = $name; } public function setSurname(string $surname) { echo "Set user surname to ", $surname, PHP_EOL; $this->surname = $surname; } public function setPassword(string $password) { if (!$password) { throw new \InvalidArgumentException("Password shouldn't be empty"); } echo "Set user password to ", $password, PHP_EOL; $this->password = $password; } } demos/Demo/Highlighter.php000064400000001453144760133060011515 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Demo; /** * Highlighter utility class */ final class Highlighter { /** * Highlighter with built-in check for list of disabled function (Google AppEngine) * * @param string $file Name of the file */ public static function highlight($file) { $highlightFileFunc = new \ReflectionFunction('highlight_file'); if (!$highlightFileFunc->isDisabled()) { highlight_file($file); } else { echo '
' . htmlspecialchars(file_get_contents($file)) . '
'; } } } demos/autoload_aspect.php000064400000001122144760133060011533 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ use Demo\Aspect\AwesomeAspectKernel; use Go\Aop\Features; include __DIR__ . '/../vendor/autoload.php'; // Initialize demo aspect container AwesomeAspectKernel::getInstance()->init([ 'debug' => true, 'appDir' => __DIR__ . '/../demos', 'cacheDir' => __DIR__ . '/cache', 'features' => Features::INTERCEPT_FUNCTIONS, ]); demos/cache/.gitignore000064400000000002144760133060010702 0ustar00* demos/cache/.gitkeep000064400000000000144760133060010341 0ustar00demos/index.php000064400000027471144760133060007512 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ ?> Go! AOP Demo

Welcome

This demo shows your an examples of AOP usage.

Please, choose one of available examples from navigation menu.
You can also try to run this code with XDebug.

getReport('Test'); // First call will take 0.1 second
        echo "Result is: ", $result, PHP_EOL;

        $result = $example->getReport('Test1'); // This call is cached and result should be 'Test'
        echo "Result is: ", $result, PHP_EOL;
        break;

    case 'loggable':
        $aspectName = 'Demo\Aspect\LoggingAspect';

        $example = new LoggingDemo();
        $example->execute('LoggingTask'); // Logging for dynamic methods
        LoggingDemo::runByName('StaticTask'); // Logging for static methods
        break;

    case 'property-interceptor':
        $aspectName = 'Demo\Aspect\PropertyInterceptorAspect';

        $example = new PropertyDemo();
        echo $example->publicProperty; // Read public property
        $example->publicProperty = 987; // Write public property
        $example->showProtected();
        $example->setProtected(987);
        break;

    case 'dynamic-interceptor':
        $aspectName = 'Demo\Aspect\DynamicMethodsAspect';

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

    case 'function-interceptor':
        $aspectName = 'Demo\Aspect\FunctionInterceptorAspect';

        $example = new FunctionDemo();
        $example->testArrayFunctions(['test' => 1, 'code' => 2, 'more' => 1]);
        $example->testFileContent();
        break;

    case 'fluent-interface':
        $aspectName = 'Demo\Aspect\FluentInterfaceAspect';

        $example = new UserFluentDemo(); // Original class doesn't provide fluent interface for us
        if ($example instanceof \Go\Aop\Proxy) { // This check is to prevent fatal errors when AOP is disabled
            $example
                ->setName('John')
                ->setSurname('Doe')
                ->setPassword('root');
        } else {
            echo "Fluent interface is not available without AOP", PHP_EOL;
        }
        break;

    case 'human-advices':
        $aspectName = 'Demo\Aspect\HealthyLiveAspect';

        $example = new HumanDemo();
        echo "Want to eat something, let's have a breakfast!", PHP_EOL;
        $example->eat();
        echo "I should work to earn some money", PHP_EOL;
        $example->work();
        echo "It was a nice day, go to bed", PHP_EOL;
        $example->sleep();
        break;

    case 'dynamic-traits':
        $aspectName = 'Demo\Aspect\IntroductionAspect';

        $example = new IntroductionDemo(); // Original class doesn't implement Serializable
        $example->testSerializable();
        break;

    case 'declare-errors':
        $aspectName = 'Demo\Aspect\DeclareErrorAspect';

        $example = new ErrorDemo();
        $example->oldMethod();
        $example->notSoGoodMethod();
        break;

    default:
}
?>
  
getFileName())); ?>
getFileName(); $basename = basename($path); $explodedPath = explode($basename, $path); $path = array_shift($explodedPath) . $basename; Highlighter::highlight(MagicConstantTransformer::resolveFileName($path)); ?>
nitpick.json000064400000000044144760133060007102 0ustar00{ "ignore": [ "tests/*" ] } phpunit.xml.dist000064400000002131144760133060007720 0ustar00 ./tests/Go/ ./src/ performance src/Aop/Advice.php000064400000000626144760133060007766 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Tag interface for Advice. Implementations can be any type of advice, such as Interceptors. * * @api */ interface Advice { } src/Aop/AdviceAfter.php000064400000000631144760133060010744 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Tag class for all after advices either for field access or method calling * * @api */ interface AdviceAfter extends Advice { } src/Aop/AdviceAround.php000064400000000633144760133060011135 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Tag class for all around advices either for field access or method calling * * @api */ interface AdviceAround extends Advice { } src/Aop/AdviceBefore.php000064400000000633144760133060011107 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Tag class for all before advices either for field access or method calling * * @api */ interface AdviceBefore extends Advice { } src/Aop/Advisor.php000064400000001065144760133060010200 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Base interface holding AOP advice (action to take at a joinpoint) */ interface Advisor { /** * Return the advice part of this aspect. An advice may be an interceptor, a before advice, a throws advice, etc. * * @api */ public function getAdvice(): Advice; } src/Aop/Aspect.php000064400000001144144760133060010006 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Aspect interface marker * * Aspect is a module that encapsulates a concern. An aspect is composed of pointcuts, advice bodies and inter-type * declarations. In some approaches, and aspect may also contain classes and methods. * * @api * @link http://en.wikipedia.org/wiki/Aspect_%28computer_science%29 */ interface Aspect { } src/Aop/AspectException.php000064400000000627144760133060011672 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; use RuntimeException; /** * Superclass for all AOP infrastructure exceptions */ class AspectException extends RuntimeException { } src/Aop/Features.php000064400000002562144760133060010352 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Interface-enumeration of framework features to use in checking and configuration */ interface Features { /** * Enables interception of system function. * By default this feature is disabled, because this option is very expensive. */ public const INTERCEPT_FUNCTIONS = 1; /** * Enables interception of "new" operator in the source code * By default this feature is disabled, because it's very tricky */ public const INTERCEPT_INITIALIZATIONS = 2; /** * Enables interception of "include"/"require" operations in legacy code * By default this feature is disabled, because only composer should be used */ public const INTERCEPT_INCLUDES = 4; /** * Do not check the cache presence and assume that cache is already prepared * * This flag is usable for read-only file systems (GAE, phar, etc) */ public const PREBUILT_CACHE = 64; /** * Enables usage of parameter widening for PHP>=7.2.0 * * @see https://wiki.php.net/rfc/parameter-no-type-variance */ public const PARAMETER_WIDENING = 128; } src/Aop/Framework/AbstractInterceptor.php000064400000010710144760133060014505 0ustar00 * * 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\Core\AspectKernel; use ReflectionFunction; use ReflectionMethod; use Serializable; use Go\Aop\Intercept\Interceptor; use function get_class; /** * Base class for all framework interceptor implementations * * This class describe an action taken by the interceptor at a particular joinpoint. * Different types of interceptors include "around", "before" and "after" advices. * * Around interceptor is an advice that surrounds a joinpoint such as a method invocation. This is the most powerful * kind of advice. Around advices will perform custom behavior before and after the method invocation. They are * responsible for choosing whether to proceed to the joinpoint or to shortcut executing by returning their own return * value or throwing an exception. * * After and before interceptors are simple closures that will be invoked after and before main invocation. * * Framework models an interceptor as an PHP-closure, maintaining a chain of interceptors "around" the joinpoint: * public function (Joinpoint $joinPoint) * { * echo 'Before action'; * // call chain here with Joinpoint->proceed() method * $result = $joinPoint->proceed(); * echo 'After action'; * * return $result; * } */ abstract class AbstractInterceptor implements Interceptor, OrderedAdvice, Serializable { /** * Local cache of advices for faster unserialization on big projects * * @var Closure[] */ protected static $localAdvicesCache = []; /** * Pointcut expression string which was used for this interceptor */ protected $pointcutExpression; /** * Closure to call */ protected $adviceMethod; /** * Advice order */ private $adviceOrder; /** * Default constructor for interceptor */ public function __construct(Closure $adviceMethod, int $adviceOrder = 0, string $pointcutExpression = '') { $this->adviceMethod = $adviceMethod; $this->adviceOrder = $adviceOrder; $this->pointcutExpression = $pointcutExpression; } /** * Serialize advice method into array */ public static function serializeAdvice(Closure $adviceMethod): array { $refAdvice = new ReflectionFunction($adviceMethod); return [ 'method' => $refAdvice->name, 'class' => $refAdvice->getClosureScopeClass()->name ]; } /** * Unserialize an advice * * @param array $adviceData Information about advice */ public static function unserializeAdvice(array $adviceData): Closure { $aspectName = $adviceData['class']; $methodName = $adviceData['method']; if (!isset(static::$localAdvicesCache["$aspectName->$methodName"])) { $aspect = AspectKernel::getInstance()->getContainer()->getAspect($aspectName); $refMethod = new ReflectionMethod($aspectName, $methodName); $advice = $refMethod->getClosure($aspect); static::$localAdvicesCache["$aspectName->$methodName"] = $advice; } return static::$localAdvicesCache["$aspectName->$methodName"]; } /** * Returns the advice order */ public function getAdviceOrder(): int { return $this->adviceOrder; } /** * Getter for extracting the advice closure from Interceptor */ public function getRawAdvice(): Closure { return $this->adviceMethod; } /** * Serializes an interceptor into string representation */ final public function serialize(): string { $vars = array_filter(get_object_vars($this)); $vars['adviceMethod'] = static::serializeAdvice($this->adviceMethod); return serialize($vars); } /** * Unserialize an interceptor from the string * * @param string $serialized The string representation of the object. */ final public function unserialize($serialized): void { $vars = unserialize($serialized, ['allowed_classes' => false]); $vars['adviceMethod'] = static::unserializeAdvice($vars['adviceMethod']); foreach ($vars as $key => $value) { $this->$key = $value; } } } src/Aop/Framework/AbstractInvocation.php000064400000001705144760133060014324 0ustar00 * * 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\Invocation; /** * Abstract class for all invocations joinpoints */ abstract class AbstractInvocation extends AbstractJoinpoint implements Invocation { /** * Arguments for invocation * * @var array */ protected $arguments = []; /** * Get the arguments as an array object. */ public function getArguments(): array { return $this->arguments; } /** * Sets the arguments for current invocation * * @api * * @param array $arguments New list of arguments */ public function setArguments(array $arguments): void { $this->arguments = $arguments; } } src/Aop/Framework/AbstractJoinpoint.php000064400000006435144760133060014171 0ustar00 * * 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\Advice; use Go\Aop\AdviceAfter; use Go\Aop\AdviceAround; use Go\Aop\AdviceBefore; use Go\Aop\Intercept\Interceptor; use Go\Aop\Intercept\Joinpoint; use function is_array; /** * Abstract joinpoint for framework * * Join points are points in the execution of the system, such as method calls, * where behavior supplied by aspects is combined. A join point is a point in * the execution of the program, which is used to define the dynamic structure * of a crosscutting concern. * * @link http://en.wikipedia.org/wiki/Aspect-oriented_software_development#Join_point_model */ abstract class AbstractJoinpoint implements Joinpoint { /** * List of advices * * @var array|Advice[]|Interceptor[] */ protected $advices = []; /** * Current advice index * * @var int */ protected $current = 0; /** * Stack frames to work with recursive calls or with cross-calls inside object * * @var array */ protected $stackFrames = []; /** * Recursion level for invocation * * @var int */ protected $level = 0; /** * Initializes list of advices for current joinpoint * * @param array $advices List of advices */ public function __construct(array $advices) { $this->advices = $advices; } /** * Sorts advices by priority * * @param array|Advice[] $advices * @return array|Advice[] Sorted list of advices */ public static function sortAdvices(array $advices): array { $sortedAdvices = $advices; uasort($sortedAdvices, function (Advice $first, Advice $second) { switch (true) { case $first instanceof AdviceBefore && !($second instanceof AdviceBefore): return -1; case $first instanceof AdviceAround && !($second instanceof AdviceAround): return 1; case $first instanceof AdviceAfter && !($second instanceof AdviceAfter): return $second instanceof AdviceBefore ? 1 : -1; case ($first instanceof OrderedAdvice && $second instanceof OrderedAdvice): return $first->getAdviceOrder() - $second->getAdviceOrder(); default: return 0; } }); return $sortedAdvices; } /** * Replace concrete advices with list of ids * * @param Advice[][][] $advices List of advices */ public static function flatAndSortAdvices(array $advices): array { $flattenAdvices = []; foreach ($advices as $type => $typedAdvices) { foreach ($typedAdvices as $name => $concreteAdvices) { if (is_array($concreteAdvices)) { $flattenAdvices[$type][$name] = array_keys(self::sortAdvices($concreteAdvices)); } else { $flattenAdvices[$type][$name] = $concreteAdvices; } } } return $flattenAdvices; } } src/Aop/Framework/AbstractMethodInvocation.php000064400000007103144760133060015463 0ustar00 * * 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\MethodInvocation; use Go\Aop\Support\AnnotatedReflectionMethod; use ReflectionMethod; use function array_merge; use function array_pop; use function count; use function is_string; /** * Abstract method invocation implementation */ abstract class AbstractMethodInvocation extends AbstractInvocation implements MethodInvocation { /** * Instance of object for invoking * * @var object|null */ protected $instance; /** * Instance of reflection method for class * * @var ReflectionMethod */ protected $reflectionMethod; /** * Class name scope for static invocation * * @var string */ protected $scope; /** * This static variable holds the name of field to use to avoid extra "if" section in the __invoke method * * Overridden in children classes and initialized via LSB * * @var string */ protected static $propertyName; /** * Constructor for method invocation * * @param array $advices List of advices for this invocation */ public function __construct(string $className, string $methodName, array $advices) { parent::__construct($advices); $this->reflectionMethod = $method = new AnnotatedReflectionMethod($className, $methodName); // Give an access to call protected method if ($method->isProtected()) { $method->setAccessible(true); } } /** * Invokes current method invocation with all interceptors * * @param null|object|string $instance Invocation instance (class name for static methods) * @param array $arguments List of arguments for method invocation * @param array $variadicArguments Additional list of variadic arguments * * @return mixed Result of invocation */ final public function __invoke($instance = null, array $arguments = [], array $variadicArguments = []) { if ($this->level > 0) { $this->stackFrames[] = [$this->arguments, $this->scope, $this->instance, $this->current]; } if (count($variadicArguments) > 0) { $arguments = array_merge($arguments, $variadicArguments); } try { ++$this->level; $this->current = 0; $this->arguments = $arguments; $this->{static::$propertyName} = $instance; return $this->proceed(); } finally { --$this->level; if ($this->level > 0) { [$this->arguments, $this->scope, $this->instance, $this->current] = array_pop($this->stackFrames); } else { $this->instance = null; $this->arguments = []; } } } /** * Gets the method being called. * * @return ReflectionMethod|AnnotatedReflectionMethod the method being called. */ public function getMethod(): ReflectionMethod { return $this->reflectionMethod; } /** * Returns friendly description of this joinpoint */ final public function __toString(): string { return sprintf( 'execution(%s%s%s())', $this->getScope(), $this->reflectionMethod->isStatic() ? '::' : '->', $this->reflectionMethod->name ); } } src/Aop/Framework/AfterInterceptor.php000064400000001266144760133060014011 0ustar00 * * 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\AdviceAfter; use Go\Aop\Intercept\Joinpoint; /** * "After" interceptor * * @api */ final class AfterInterceptor extends AbstractInterceptor implements AdviceAfter { /** * @inheritdoc */ public function invoke(Joinpoint $joinpoint) { try { return $joinpoint->proceed(); } finally { ($this->adviceMethod)($joinpoint); } } } src/Aop/Framework/AfterThrowingInterceptor.php000064400000001515144760133060015530 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Framework; use Exception; use Go\Aop\AdviceAfter; use Go\Aop\Intercept\Joinpoint; /** * "After Throwing" interceptor * * @api */ final class AfterThrowingInterceptor extends AbstractInterceptor implements AdviceAfter { /** * @inheritdoc * @throws Exception */ public function invoke(Joinpoint $joinpoint) { try { return $joinpoint->proceed(); } catch (Exception $invocationException) { ($this->adviceMethod)($joinpoint, $invocationException); throw $invocationException; } } } src/Aop/Framework/AroundInterceptor.php000064400000001147144760133060014176 0ustar00 * * 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\AdviceAround; use Go\Aop\Intercept\Joinpoint; /** * "Around" interceptor * * @api */ final class AroundInterceptor extends AbstractInterceptor implements AdviceAround { /** * @inheritdoc */ public function invoke(Joinpoint $joinpoint) { return ($this->adviceMethod)($joinpoint); } } src/Aop/Framework/BeforeInterceptor.php000064400000001207144760133060014145 0ustar00 * * 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\AdviceBefore; use Go\Aop\Intercept\Joinpoint; /** * "Before" interceptor * * @api */ final class BeforeInterceptor extends AbstractInterceptor implements AdviceBefore { /** * @inheritdoc */ public function invoke(Joinpoint $joinpoint) { ($this->adviceMethod)($joinpoint); return $joinpoint->proceed(); } } src/Aop/Framework/ClassFieldAccess.php000064400000014324144760133060013663 0ustar00 * * 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\AspectException; use Go\Aop\Intercept\FieldAccess; use Go\Aop\Support\AnnotatedReflectionProperty; use ReflectionProperty; use function get_class; /** * Represents a field access joinpoint */ class ClassFieldAccess extends AbstractJoinpoint implements FieldAccess { /** * Instance of object for accessing * * @var object */ protected $instance; /** * Instance of reflection property * * @var ReflectionProperty */ protected $reflectionProperty; /** * New value to set * * @var mixed */ protected $newValue; /** * Access type for field access * * @var integer */ private $accessType; /** * Copy of the original value of property * * @var mixed */ private $value; /** * Constructor for field access * * @param array $advices List of advices for this invocation */ public function __construct(string $className, string $fieldName, array $advices) { parent::__construct($advices); $this->reflectionProperty = $reflectionProperty = new AnnotatedReflectionProperty($className, $fieldName); // Give an access to protected field if ($reflectionProperty->isProtected()) { $reflectionProperty->setAccessible(true); } } /** * Returns the access type. */ public function getAccessType(): int { return $this->accessType; } /** * Gets the field being accessed. * * @return AnnotatedReflectionProperty the property which is being accessed. */ public function getField(): ReflectionProperty { return $this->reflectionProperty; } /** * Gets the current value of property */ public function &getValue() { $value = &$this->value; return $value; } /** * Gets the value that must be set to the field. */ public function &getValueToSet() { $newValue = &$this->newValue; return $newValue; } /** * Checks scope rules for accessing property */ public function ensureScopeRule(int $stackLevel = 2): void { $property = $this->reflectionProperty; $isProtected = $property->isProtected(); $isPrivate = $property->isPrivate(); if ($isProtected || $isPrivate) { $backTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $stackLevel+1); $accessor = $backTrace[$stackLevel] ?? []; $propertyClass = $property->class; if (isset($accessor['class'])) { // For private and protected properties its ok to access from the same class if ($accessor['class'] === $propertyClass) { return; } // For protected properties its ok to access from any subclass if ($isProtected && is_subclass_of($accessor['class'], $propertyClass)) { return; } } throw new AspectException("Cannot access property {$propertyClass}::{$property->name}"); } } /** * Proceed to the next interceptor in the Chain * * @return void For field interceptor there is no return values */ final public function proceed() { if (isset($this->advices[$this->current])) { $currentInterceptor = $this->advices[$this->current++]; $currentInterceptor->invoke($this); } } /** * Invokes current field access with all interceptors * * @param object $instance Instance of object * @param integer $accessType Type of access: READ or WRITE * @param mixed $originalValue Original value of property * @param mixed $newValue New value to set * * @return mixed */ final public function &__invoke(object $instance, int $accessType, &$originalValue, $newValue = NAN) { if ($this->level > 0) { $this->stackFrames[] = [$this->instance, $this->accessType, &$this->value, &$this->newValue]; } try { ++$this->level; $this->current = 0; $this->instance = $instance; $this->accessType = $accessType; $this->value = &$originalValue; $this->newValue = $newValue; $this->proceed(); if ($accessType === self::READ) { $result = &$this->value; } else { $result = &$this->newValue; } return $result; } finally { --$this->level; if ($this->level > 0) { [$this->instance, $this->accessType, $this->value, $this->newValue] = array_pop($this->stackFrames); } } } /** * Returns the object for which current joinpoint is invoked * * @return object|null Instance of object or null for static call/unavailable context */ final public function getThis(): ?object { return $this->instance; } /** * Checks if the current joinpoint is dynamic or static * * Dynamic joinpoint contains a reference to an object that can be received via getThis() method call * * @see ClassJoinpoint::getThis() */ final public function isDynamic(): bool { return true; } /** * Returns the static scope name (class name) of this joinpoint. */ final public function getScope(): string { // $this->instance always contains an object, so we can take it's name as a scope name return get_class($this->instance); } /** * Returns a friendly description of current joinpoint */ final public function __toString(): string { return sprintf( '%s(%s->%s)', $this->accessType === self::READ ? 'get' : 'set', $this->getScope(), $this->reflectionProperty->name ); } } src/Aop/Framework/DeclareErrorInterceptor.php000064400000004466144760133060015326 0ustar00 * * 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\FieldAccess; use Go\Aop\Intercept\Joinpoint; /** * Interceptor to dynamically trigger an 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. */ final class DeclareErrorInterceptor extends AbstractInterceptor { /** * Error message to show for this interceptor * * @var string */ protected $message; /** * Default level of error * * @var int */ protected $level; /** * Default constructor for interceptor */ public function __construct(string $message, int $errorLevel, string $pointcutExpression) { $adviceMethod = self::getDeclareErrorAdvice(); $this->message = $message; $this->level = $errorLevel; parent::__construct($adviceMethod, -256, $pointcutExpression); } /** * @inheritdoc */ public static function unserializeAdvice(array $adviceData): Closure { return self::getDeclareErrorAdvice(); } /** * @inheritdoc */ public function invoke(Joinpoint $joinpoint) { if ($joinpoint instanceof FieldAccess) { $scope = $joinpoint->getScope(); $name = $joinpoint->getField()->getName(); ($this->adviceMethod)($scope, $name, $this->message, $this->level); } return $joinpoint->proceed(); } /** * Returns an advice */ private static function getDeclareErrorAdvice(): Closure { static $adviceMethod; if (!$adviceMethod) { $adviceMethod = function (string $scope, string $property, string $message, int $level = E_USER_NOTICE) { $message = vsprintf('[AOP Declare Error]: %s has an error: "%s"', [ $scope . '->' . $property, $message ]); trigger_error($message, $level); }; } return $adviceMethod; } } src/Aop/Framework/DynamicClosureMethodInvocation.php000064400000005256144760133060016650 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Framework; use function get_class; /** * Dynamic closure method invocation is responsible to call dynamic methods via closure */ final class DynamicClosureMethodInvocation extends AbstractMethodInvocation { /** * Closure to use * * @var \Closure */ protected $closureToCall; /** * Previous instance of invocation * * @var null|object */ protected $previousInstance; /** * For dynamic calls we store given argument as 'instance' property */ protected static $propertyName = 'instance'; /** * Invokes original method and return result from it * * @return mixed */ public function proceed() { if (isset($this->advices[$this->current])) { $currentInterceptor = $this->advices[$this->current++]; return $currentInterceptor->invoke($this); } // Fill the closure only once if it's empty if ($this->closureToCall === null) { $this->closureToCall = $this->reflectionMethod->getClosure($this->instance); $this->previousInstance = $this->instance; } // Rebind the closure if instance was changed since last time if ($this->previousInstance !== $this->instance) { $this->closureToCall = $this->closureToCall->bindTo($this->instance, $this->reflectionMethod->class); $this->previousInstance = $this->instance; } return ($this->closureToCall)(...$this->arguments); } /** * Returns the object for which current joinpoint is invoked * * @return object Instance of object or null for static call/unavailable context */ final public function getThis(): ?object { return $this->instance; } /** * Checks if the current joinpoint is dynamic or static * * Dynamic joinpoint contains a reference to an object that can be received via getThis() method call * @see ClassJoinpoint::getThis() */ final public function isDynamic(): bool { return true; } /** * Returns the static scope name (class name) of this joinpoint. */ final public function getScope(): string { // Due to optimization $this->scope won't be filled for each invocation // However, $this->instance always contains an object, so we can take it's name as a scope name return get_class($this->instance); } } src/Aop/Framework/DynamicInvocationMatcherInterceptor.php000064400000003462144760133060017672 0ustar00 * * 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\Interceptor; use Go\Aop\Intercept\Joinpoint; use Go\Aop\Intercept\MethodInvocation; use Go\Aop\PointFilter; use ReflectionClass; /** * Dynamic invocation matcher combines a pointcut and interceptor. * * For each invocation interceptor asks the pointcut if it matches the invocation. * Matcher will receive reflection point, object instance and invocation arguments to make a decision */ class DynamicInvocationMatcherInterceptor implements Interceptor { /** * Instance of pointcut to dynamically match joinpoints with args * * @var PointFilter */ protected $pointFilter; /** * Instance of interceptor to invoke * * @var Interceptor */ protected $interceptor; /** * Dynamic matcher constructor */ public function __construct(PointFilter $pointFilter, Interceptor $interceptor) { $this->pointFilter = $pointFilter; $this->interceptor = $interceptor; } /** * @inheritdoc */ final public function invoke(Joinpoint $joinpoint) { if ($joinpoint instanceof MethodInvocation) { $method = $joinpoint->getMethod(); $context = $joinpoint->getThis() ?? $joinpoint->getScope(); $contextClass = new ReflectionClass($context); if ($this->pointFilter->matches($method, $contextClass, $context, $joinpoint->getArguments())) { return $this->interceptor->invoke($joinpoint); } } return $joinpoint->proceed(); } } src/Aop/Framework/OrderedAdvice.php000064400000000773144760133060013233 0ustar00 * * 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\Advice; /** * Ordered advice can have a custom order to implement sorting */ interface OrderedAdvice extends Advice { /** * Returns the advice order */ public function getAdviceOrder(): int; } src/Aop/Framework/ReflectionConstructorInvocation.php000064400000007736144760133060017133 0ustar00 * * 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\ConstructorInvocation; use Go\Core\AspectContainer; use ReflectionClass; use ReflectionMethod; /** * Reflection constructor invocation implementation */ class ReflectionConstructorInvocation extends AbstractInvocation implements ConstructorInvocation { /** * Reflection class * * @var ReflectionClass */ protected $class; /** * Instance of created class, can be used for Around or After types of advices * * @var object|null */ protected $instance; /** * Instance of reflection constructor for class * * @var null|ReflectionMethod */ private $constructor; /** * Constructor for constructor invocation :) * * @param array $advices List of advices for this invocation */ public function __construct(string $className, string $unusedType, array $advices) { $originalClass = $className; if (strpos($originalClass, AspectContainer::AOP_PROXIED_SUFFIX) !== false) { $originalClass = substr($originalClass, 0, -strlen(AspectContainer::AOP_PROXIED_SUFFIX)); } $this->class = new ReflectionClass($originalClass); $this->constructor = $constructor = $this->class->getConstructor(); // Give an access to call protected/private constructors if ($constructor && !$constructor->isPublic()) { $constructor->setAccessible(true); } parent::__construct($advices); } /** * Proceed to the next interceptor in the Chain * * Typically this method is called inside previous closure, as instance of Joinpoint is passed to callback * Do not call this method directly, only inside callback closures. * * @return mixed */ final public function proceed() { if (isset($this->advices[$this->current])) { $currentInterceptor = $this->advices[$this->current]; $this->current++; return $currentInterceptor->invoke($this); } $this->instance = $this->class->newInstanceWithoutConstructor(); $constructor = $this->getConstructor(); if ($constructor !== null) { $constructor->invoke($this->instance, ...$this->arguments); } return $this->instance; } /** * Gets the constructor being called or null if it is absent. */ public function getConstructor(): ?ReflectionMethod { return $this->constructor; } /** * Returns the object for which current joinpoint is invoked * * @return object|null Instance of object or null for static call/unavailable context */ public function getThis(): ?object { return $this->instance; } /** * Invokes current constructor invocation with all interceptors * * @return mixed */ final public function __invoke(array $arguments = []) { $this->current = 0; $this->arguments = $arguments; return $this->proceed(); } /** * Checks if the current joinpoint is dynamic or static * * Dynamic joinpoint contains a reference to an object that can be received via getThis() method call * * @see ClassJoinpoint::getThis() */ public function isDynamic(): bool { return true; } /** * Returns the static scope name (class name) of this joinpoint. */ public function getScope(): string { return $this->class->getName(); } /** * Returns a friendly description of current joinpoint */ final public function __toString(): string { return sprintf( 'initialization(%s)', $this->getScope() ); } } src/Aop/Framework/ReflectionFunctionInvocation.php000064400000005444144760133060016365 0ustar00 * * 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\FunctionInvocation; use ReflectionFunction; use function array_merge, array_pop; /** * Function invocation implementation */ class ReflectionFunctionInvocation extends AbstractInvocation implements FunctionInvocation { /** * Instance of reflection function * * @var ReflectionFunction */ protected $reflectionFunction; /** * Constructor for function invocation * * @param array $advices List of advices for this invocation * * @throws \ReflectionException */ public function __construct(string $functionName, array $advices) { parent::__construct($advices); $this->reflectionFunction = new ReflectionFunction($functionName); } /** * Invokes original function and return result from it * * @return mixed */ public function proceed() { if (isset($this->advices[$this->current])) { $currentInterceptor = $this->advices[$this->current++]; return $currentInterceptor->invoke($this); } return $this->reflectionFunction->invokeArgs($this->arguments); } /** * Gets the function being called. */ public function getFunction(): ReflectionFunction { return $this->reflectionFunction; } /** * Invokes current function invocation with all interceptors * * @param array $arguments List of arguments for function invocation * @param array $variadicArguments Additional list of variadic arguments * * @return mixed Result of invocation */ final public function __invoke(array $arguments = [], array $variadicArguments = []) { if ($this->level > 0) { $this->stackFrames[] = [$this->arguments, $this->current]; } if (!empty($variadicArguments)) { $arguments = array_merge($arguments, $variadicArguments); } try { ++$this->level; $this->current = 0; $this->arguments = $arguments; $result = $this->proceed(); } finally { --$this->level; if ($this->level > 0) { [$this->arguments, $this->current] = array_pop($this->stackFrames); } } return $result; } /** * Returns a friendly description of current joinpoint */ final public function __toString(): string { return sprintf( 'execution(%s())', $this->reflectionFunction->getName() ); } } src/Aop/Framework/StaticClosureMethodInvocation.php000064400000005462144760133060016512 0ustar00 * * 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; /** * Static closure method invocation is responsible to call static methods via closure */ final class StaticClosureMethodInvocation extends AbstractMethodInvocation { /** * Closure to use * * @var Closure */ protected $closureToCall; /** * Previous scope of invocation * * @var null|string */ protected $previousScope; /** * For static calls we store given argument as 'scope' property */ protected static $propertyName = 'scope'; /** * Proceeds all registered advices for the static method and returns an invocation result */ public function proceed() { if (isset($this->advices[$this->current])) { $currentInterceptor = $this->advices[$this->current++]; return $currentInterceptor->invoke($this); } // Rebind the closure if scope (class name) was changed since last time if ($this->previousScope !== $this->scope) { if ($this->closureToCall === null) { $this->closureToCall = static::getStaticInvoker($this->reflectionMethod->class, $this->reflectionMethod->name); } $this->closureToCall = $this->closureToCall->bindTo(null, $this->scope); $this->previousScope = $this->scope; } return ($this->closureToCall)($this->arguments); } /** * Returns static method invoker for the concrete method in the class */ protected static function getStaticInvoker(string $className, string $methodName): Closure { return function (array $args) use ($className, $methodName) { return forward_static_call_array([$className, $methodName], $args); }; } /** * Checks if the current joinpoint is dynamic or static * * Dynamic joinpoint contains a reference to an object that can be received via getThis() method call * * @see ClassJoinpoint::getThis() */ final public function isDynamic(): bool { return false; } /** * Returns the object for which current joinpoint is invoked * * @return object Instance of object or null for static call/unavailable context */ final public function getThis(): ?object { return null; } /** * Returns the static scope name (class name) of this joinpoint. */ final public function getScope(): string { // $this->scope contains the current class scope that was received via static::class return $this->scope; } } src/Aop/Framework/StaticInitializationJoinpoint.php000064400000005317144760133060016563 0ustar00 * * 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\ClassJoinpoint; use Go\Core\AspectContainer; use ReflectionClass; use function strlen; /** * Static initialization joinpoint is invoked after class is loaded into memory */ class StaticInitializationJoinpoint extends AbstractJoinpoint implements ClassJoinpoint { /** * @var ReflectionClass */ protected $reflectionClass; /** * Constructor for the class static initialization joinpoint * * @param array $advices List of advices for this invocation */ public function __construct(string $className, string $unusedType, array $advices) { $originalClass = $className; if (strpos($originalClass, AspectContainer::AOP_PROXIED_SUFFIX)) { $originalClass = substr($originalClass, 0, -strlen(AspectContainer::AOP_PROXIED_SUFFIX)); } $this->reflectionClass = new \ReflectionClass($originalClass); parent::__construct($advices); } /** * Proceeds to the next interceptor in the chain. * * @return void Static initializtion could not return anything */ public function proceed() { if (isset($this->advices[$this->current])) { $currentInterceptor = $this->advices[$this->current++]; $currentInterceptor->invoke($this); } } /** * Invokes current joinpoint with all interceptors */ final public function __invoke(): void { $this->current = 0; $this->proceed(); } /** * Returns the object for which current joinpoint is invoked * * @return object|null Instance of object or null for static call/unavailable context */ public function getThis(): ?object { return null; } /** * Checks if the current joinpoint is dynamic or static * * Dynamic joinpoint contains a reference to an object that can be received via getThis() method call * * @see ClassJoinpoint::getThis() */ public function isDynamic(): bool { return false; } /** * Returns the static scope name (class name) of this joinpoint. */ public function getScope(): string { return $this->reflectionClass->getName(); } /** * Returns a friendly description of current joinpoint */ final public function __toString(): string { return sprintf( 'staticinitialization(%s)', $this->getScope() ); } } src/Aop/Framework/TraitIntroductionInfo.php000064400000002433144760133060015027 0ustar00 * * 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\IntroductionInfo; /** * Advice for introduction that holds list of traits and interfaces for the concrete class */ class TraitIntroductionInfo implements IntroductionInfo { /** * Introduced interface */ private $introducedInterface; /** * Introduced trait */ private $introducedTrait; /** * Creates a TraitIntroductionInfo with given trait name and interface name. */ public function __construct(string $introducedTrait, string $introducedInterface) { $this->introducedTrait = $introducedTrait; $this->introducedInterface = $introducedInterface; } /** * Returns the additional interface introduced by this Advisor or Advice. */ public function getInterface(): string { return $this->introducedInterface; } /** * Returns the additional trait with realization of introduced interface */ public function getTrait(): string { return $this->introducedTrait; } } src/Aop/Intercept/ClassJoinpoint.php000064400000002126144760133060013464 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Intercept; /** * This interface represents a class joinpoint that can have $this variable and defined scope * * @api */ interface ClassJoinpoint extends Joinpoint { /** * Checks if the current joinpoint is dynamic or static * * Dynamic joinpoint contains a reference to an object that can be received via getThis() method call * @see ClassJoinpoint::getThis() * * @api */ public function isDynamic(): bool; /** * Returns the object for which current joinpoint is invoked * * @api * * @return object|null Instance of object or null for static call/unavailable context */ public function getThis(): ?object; /** * Returns the static scope name (class name) of this joinpoint. * * @api */ public function getScope(): string; } src/Aop/Intercept/ConstructorInvocation.php000064400000001326144760133060015105 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Intercept; use ReflectionMethod; /** * Description of an invocation to a constructor, given to an * interceptor upon constructor-call. * * A constructor invocation is a joinpoint and can be intercepted * by a constructor interceptor. */ interface ConstructorInvocation extends Invocation, ClassJoinpoint { /** * Gets the constructor being called or null if it is absent. */ public function getConstructor(): ?ReflectionMethod; } src/Aop/Intercept/FieldAccess.php000064400000002245144760133060012674 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Intercept; use ReflectionProperty; /** * This interface represents a field access in the program. * * A field access is a joinpoint and can be intercepted by a field interceptor. * * @api */ interface FieldAccess extends ClassJoinpoint { /** * The read access type */ public const READ = 0; /** * The write access type */ public const WRITE = 1; /** * Gets the field being accessed. * * @api */ public function getField(): ReflectionProperty; /** * Gets the current value of property by reference * * @api */ public function &getValue(); /** * Gets the value that must be set to the field, applicable only for WRITE access type * * @api */ public function &getValueToSet(); /** * Returns the access type. * * @api */ public function getAccessType(): int; } src/Aop/Intercept/FunctionInvocation.php000064400000002020144760133060014335 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Intercept; use ReflectionFunction; /** * Description of an invocation to a function, given to an interceptor * upon function-call. * * A function invocation is a joinpoint and can be intercepted by a function * interceptor. */ interface FunctionInvocation extends Invocation { /** * Gets the function being called. */ public function getFunction(): ReflectionFunction; /** * Invokes current function invocation with all interceptors * * @param array $arguments List of arguments for function invocation * @param array $variadicArguments Additional list of variadic arguments * * @return mixed Result of invocation */ public function __invoke(array $arguments = [], array $variadicArguments = []); } src/Aop/Intercept/Interceptor.php000064400000002662144760133060013030 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Intercept; use Go\Aop\Advice; /** * This interface represents a generic interceptor. * * A generic interceptor can intercept runtime events that occur * within a base program. Those events are materialized by (reified * in) joinpoints. Runtime joinpoints can be invocations, field * access, exceptions... * * This interface is not used directly. Use the the sub-interfaces * to intercept specific events. For instance, the following class * implements some specific interceptors in order to implement a * debugger: * *
 * class DebuggingInterceptor implements Interceptor
 * {
 *     public function invoke(Joinpoint $i)
 *     {
 *         $this->debug($i->getStaticPart(), $i->getThis(), $i->getArgs());
 *
 *         return $i->proceed();
 *     }
 *
 *     protected function debug($accessibleObject, $object, $value)
 *     {
 *         ...
 *     }
 * }
 * 
* * @see Joinpoint * @api */ interface Interceptor extends Advice { /** * Performs extra actions before and after the invocation of joinpoint. * * @api * * @return mixed The result of the call */ public function invoke(Joinpoint $joinpoint); } src/Aop/Intercept/Invocation.php000064400000001464144760133060012642 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Intercept; /** * This interface represents an invocation in the program * * An invocation is a callable joinpoint and can be intercepted by an interceptor * * @api */ interface Invocation extends Joinpoint { /** * Get the arguments of invocation as an array. * * @api */ public function getArguments(): array; /** * Sets the arguments for current invocation * * @api * * @param array $arguments New list of arguments */ public function setArguments(array $arguments): void; } src/Aop/Intercept/Joinpoint.php000064400000002464144760133060012503 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Intercept; /** * This interface represents a generic runtime joinpoint (in the AOP terminology). * * A runtime joinpoint is an event that occurs on a static joinpoint (i.e. a location in a the program). * For instance, an invocation is the runtime joinpoint on a method (static joinpoint). * The static part of a given joinpoint can be generically retrieved using the getStaticPart() method. * * In the context of an interception framework, a runtime joinpoint is then the reification of an access to * an accessible object (a method, a constructor, a field), i.e. the static part of the joinpoint. It is passed * to the interceptors that are installed on the static joinpoint. * * @see Interceptor * @api */ interface Joinpoint { /** * Proceeds to the next interceptor in the chain. * * @api * * @return mixed see the children interfaces' proceed definition. */ public function proceed(); /** * Returns a friendly description of current joinpoint */ public function __toString(): string; } src/Aop/Intercept/MethodInvocation.php000064400000002362144760133060014001 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Intercept; use Go\Aop\Support\AnnotatedReflectionMethod; use ReflectionMethod; /** * Description of an invocation to a method, given to an interceptor * upon method-call. * * A method invocation is a joinpoint and can be intercepted by a method * interceptor. */ interface MethodInvocation extends Invocation, ClassJoinpoint { /** * Gets the method being called. * * @return AnnotatedReflectionMethod the method being called. */ public function getMethod(): ReflectionMethod; /** * Invokes current method invocation with all interceptors * * @param null|object|string $instance Invocation instance (class name for static methods) * @param array $arguments List of arguments for method invocation * @param array $variadicArguments Additional list of variadic arguments * * @return mixed Result of invocation */ public function __invoke($instance = null, array $arguments = [], array $variadicArguments = []); } src/Aop/IntroductionAdvisor.php000064400000001644144760133060012605 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Superinterface for advisors that perform one or more AOP introductions. * * This interface cannot be implemented directly; subinterfaces must provide the advice type * implementing the introduction. * * Introduction is the implementation of additional interfaces (not implemented by a target) via AOP advice. */ interface IntroductionAdvisor extends Advisor { /** * Returns the filter determining which target classes this introduction should apply to. * * This represents the class part of a pointcut. Note that method matching doesn't make sense to introductions. */ public function getClassFilter(): PointFilter; } src/Aop/IntroductionInfo.php000064400000001635144760133060012071 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Interface supplying the information necessary to describe an introduction of trait. * * If an Advice implements this, it may be used as an introduction without an IntroductionAdvisor. * In this case, the advice is self-describing, providing not only the necessary behavior, * but describing the interfaces it introduces. */ interface IntroductionInfo extends Advice { /** * Returns the additional interface introduced by this Advisor or Advice. */ public function getInterface(): string; /** * Return the additional trait with realization of introduced interface */ public function getTrait(): string; } src/Aop/PointFilter.php000064400000004200144760133060011022 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Filter that restricts matching of a pointcut or introduction to a given set of reflection points. * * A PointFilter may be evaluated statically or at runtime (dynamically). * * Static matching involves point and context. Dynamic matching also provides an instance and arguments for * a particular invocation. * * If point filter is not dynamic (self::KIND_DYNAMIC), evaluation can be performed statically, * and the result will be the same for all invocations of this joinpoint, whatever their arguments. * * If an implementation returns true from its 2-arg matches() method and filter is self::KIND_DYNAMIC, * the 3-arg matches() method will be invoked immediately before each potential execution of the related advice, * to decide whether the advice should run. All previous advice, such as earlier interceptors in an interceptor chain, * will have run, so any state changes they have produced in parameters will be available at the time of evaluation. */ interface PointFilter { const KIND_METHOD = 1; const KIND_PROPERTY = 2; const KIND_CLASS = 4; const KIND_TRAIT = 8; const KIND_FUNCTION = 16; const KIND_INIT = 32; const KIND_STATIC_INIT = 64; const KIND_ALL = 127; const KIND_DYNAMIC = 256; /** * Performs matching of point of code, returns true if point matches * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool; /** * Returns the kind of point filter */ public function getKind(): int; } src/Aop/Pointcut.php000064400000001332144760133060010373 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Pointcut realization for PHP * * Pointcuts are defined as a predicate over the syntax-tree of the program, and define an interface that constrains * which elements of the base program are exposed by the pointcut. A pointcut picks out certain join points and values * at those points */ interface Pointcut extends PointFilter { /** * Return the class filter for this pointcut. */ public function getClassFilter(): PointFilter; } src/Aop/Pointcut/AndPointcut.php000064400000005027144760133060012630 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; use Go\Aop\Support\AndPointFilter; /** * Logical "AND" pointcut that combines two simple pointcuts */ class AndPointcut implements Pointcut { use PointcutClassFilterTrait; /** * First pointcut */ protected $first; /** * Second pointcut */ protected $second; /** * Returns pointcut kind */ protected $kind = 0; /** * "And" pointcut constructor */ public function __construct(Pointcut $first, Pointcut $second) { $this->first = $first; $this->second = $second; $this->kind = $first->getKind() & $second->getKind(); $this->classFilter = new AndPointFilter($first->getClassFilter(), $second->getClassFilter()); } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { return $this->matchPart($this->first, $point, $context, $instance, $arguments) && $this->matchPart($this->second, $point, $context, $instance, $arguments); } /** * Returns the kind of point filter */ public function getKind(): int { return $this->kind; } /** * Checks if point filter matches the point * * @param Pointcut $pointcut * @param \ReflectionMethod|\ReflectionProperty $point * @param mixed $context Related context, can be class or namespace * @param object|string|null $instance [Optional] Instance for dynamic matching * @param array $arguments [Optional] Extra arguments for dynamic matching * * @return bool */ protected function matchPart( Pointcut $pointcut, $point, $context = null, $instance = null, array $arguments = null ): bool { return $pointcut->matches($point, $context, $instance, $arguments) && $pointcut->getClassFilter()->matches($context); } } src/Aop/Pointcut/AnnotationPointcut.php000064400000005426144760133060014243 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Doctrine\Common\Annotations\Reader; use Go\Aop\Pointcut; use InvalidArgumentException; use ReflectionClass; use ReflectionMethod; use ReflectionProperty; /** * Annotation property pointcut checks property annotation */ class AnnotationPointcut implements Pointcut { use PointcutClassFilterTrait; /** * Annotation class to match */ protected $annotationName; /** * Annotation reader * * @var null|Reader */ protected $annotationReader; /** * Kind of current filter, can be KIND_CLASS, KIND_METHOD, KIND_PROPERTY, KIND_TRAIT */ protected $filterKind = 0; /** * Specifies name of the expected class to receive * * @var string */ protected $expectedClass = ''; /** * Method to call for annotation reader * * @var string */ protected $annotationMethod = ''; /** * Static mappings of kind to expected class and method name * * @var array */ protected static $mappings = [ self::KIND_CLASS => [ReflectionClass::class, 'getClassAnnotation'], self::KIND_TRAIT => [ReflectionClass::class, 'getClassAnnotation'], self::KIND_METHOD => [ReflectionMethod::class, 'getMethodAnnotation'], self::KIND_PROPERTY => [ReflectionProperty::class, 'getPropertyAnnotation'] ]; /** * Annotation matcher constructor * * @param integer $filterKind Kind of filter, e.g. KIND_CLASS */ public function __construct(int $filterKind, Reader $annotationReader, string $annotationName) { if (!isset(self::$mappings[$filterKind])) { throw new InvalidArgumentException("Unsupported filter kind {$filterKind}"); } $this->filterKind = $filterKind; $this->annotationName = $annotationName; $this->annotationReader = $annotationReader; [$this->expectedClass, $this->annotationMethod] = self::$mappings[$filterKind]; } /** * {@inheritdoc} */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { $expectedClass = $this->expectedClass; if (!$point instanceof $expectedClass) { return false; } $annotation = $this->annotationReader->{$this->annotationMethod}($point, $this->annotationName); return (bool) $annotation; } /** * Returns the kind of point filter */ public function getKind(): int { return $this->filterKind; } } src/Aop/Pointcut/CFlowBelowMethodPointcut.php000064400000005101144760133060015263 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use ReflectionClass; use Go\Aop\Pointcut; use Go\Aop\PointFilter; use ReflectionMethod; /** * Flow pointcut is a dynamic checker that verifies stack trace to understand is it matches or not */ class CFlowBelowMethodPointcut implements PointFilter, Pointcut { use PointcutClassFilterTrait; /** * Filter for the class */ protected $internalClassFilter; /** * Filter for the points */ protected $internalPointFilter; /** * Control flow below constructor * * @throws \InvalidArgumentException if filter doesn't support methods */ public function __construct(Pointcut $pointcut) { $this->internalClassFilter = $pointcut->getClassFilter(); $this->internalPointFilter = $pointcut; if (!($this->internalPointFilter->getKind() & PointFilter::KIND_METHOD)) { throw new \InvalidArgumentException('Only method filters are valid for control flow'); } } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { // With single parameter (statically) always matches if ($instance === null) { return true; } $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); foreach ($trace as $stackFrame) { if (!isset($stackFrame['class'])) { continue; } $refClass = new ReflectionClass($stackFrame['class']); if (!$this->internalClassFilter->matches($refClass)) { continue; } $refMethod = $refClass->getMethod($stackFrame['function']); if ($this->internalPointFilter->matches($refMethod)) { return true; } } return false; } /** * Returns the kind of point filter */ public function getKind(): int { return PointFilter::KIND_METHOD | PointFilter::KIND_DYNAMIC; } } src/Aop/Pointcut/ClassMemberReference.php000064400000004344144760133060014415 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\PointFilter; use Go\Aop\Support\ModifierMatcherFilter; /** * Data transfer object for storing a reference to the class member (property or method) */ class ClassMemberReference { /** * Filter for class names */ private $classFilter; /** * Member visibility filter (public/protected/etc) */ private $visibilityFilter; /** * Filter for access type (statically "::" or dynamically "->") */ private $accessTypeFilter; /** * Member name pattern */ private $memberNamePattern; /** * Default constructor * * @param PointFilter $classFilter * @param ModifierMatcherFilter $visibilityFilter Public/protected/etc * @param ModifierMatcherFilter $accessTypeFilter Static or dynamic * @param string $memberNamePattern Expression for the name */ public function __construct( PointFilter $classFilter, ModifierMatcherFilter $visibilityFilter, ModifierMatcherFilter $accessTypeFilter, string $memberNamePattern ) { $this->classFilter = $classFilter; $this->visibilityFilter = $visibilityFilter; $this->accessTypeFilter = $accessTypeFilter; $this->memberNamePattern = $memberNamePattern; } /** * Returns the filter for class */ public function getClassFilter(): PointFilter { return $this->classFilter; } /** * Returns the filter for visibility: public/protected/private */ public function getVisibilityFilter(): ModifierMatcherFilter { return $this->visibilityFilter; } /** * Returns the filter for access type: static/dynamic */ public function getAccessTypeFilter(): ModifierMatcherFilter { return $this->accessTypeFilter; } /** * Returns the pattern for member name */ public function getMemberNamePattern(): string { return $this->memberNamePattern; } } src/Aop/Pointcut/FunctionPointcut.php000064400000005046144760133060013714 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use ReflectionFunction; use Go\Aop\Pointcut; use Go\Aop\PointFilter; /** * Function pointcut checks function signature (namespace and name) to match it */ class FunctionPointcut implements Pointcut { /** * @var PointFilter */ protected $nsFilter; /** * Function name to match, can contain wildcards *,? */ protected $functionName = ''; /** * Regular expression for matching */ protected $regexp; /** * Additional return type filter (if present) */ protected $returnTypeFilter; /** * Function matcher constructor */ public function __construct(string $functionName, PointFilter $returnTypeFilter = null) { $this->functionName = $functionName; $this->returnTypeFilter = $returnTypeFilter; $this->regexp = strtr(preg_quote($this->functionName, '/'), [ '\\*' => '.*?', '\\?' => '.' ]); } /** * Performs matching of point of code * * @param mixed $function Specific part of code, can be any Reflection class * @param mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($function, $context = null, $instance = null, array $arguments = null): bool { if (!$function instanceof ReflectionFunction) { return false; } if (($this->returnTypeFilter !== null) && !$this->returnTypeFilter->matches($function, $context)) { return false; } return ($function->name === $this->functionName) || (bool) preg_match("/^{$this->regexp}$/", $function->name); } /** * Returns the kind of point filter */ public function getKind(): int { return self::KIND_FUNCTION; } /** * Return the class filter for this pointcut. */ public function getClassFilter(): PointFilter { return $this->nsFilter; } /** * Configures the namespace filter, used as pre-filter for functions */ public function setNamespaceFilter(PointFilter $nsFilter): void { $this->nsFilter = $nsFilter; } } src/Aop/Pointcut/MagicMethodPointcut.php000064400000004703144760133060014307 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; use Go\Aop\PointFilter; /** * Magic method pointcut is a dynamic checker that verifies calls for __call and __callStatic */ class MagicMethodPointcut implements PointFilter, Pointcut { use PointcutClassFilterTrait; /** * Method name to match, can contain wildcards *,? */ protected $methodName = ''; /** * Regular expression for matching */ protected $regexp; /** * Modifier filter for method */ protected $modifierFilter; /** * Magic method matcher constructor * * NB: only public methods can be matched as __call and __callStatic are public */ public function __construct(string $methodName, PointFilter $modifierFilter) { $this->methodName = $methodName; $this->regexp = strtr(preg_quote($this->methodName, '/'), [ '\\*' => '.*?', '\\?' => '.', '\\|' => '|' ]); $this->modifierFilter = $modifierFilter; } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { // With single parameter (statically) always matches for __call, __callStatic if ($instance === null) { return ($point->name === '__call' || $point->name === '__callStatic'); } if (!$this->modifierFilter->matches($point)) { return false; } // for __call and __callStatic method name is the first argument on invocation [$methodName] = $arguments; return ($methodName === $this->methodName) || (bool) preg_match("/^(?:{$this->regexp})$/", $methodName); } /** * Returns the kind of point filter */ public function getKind(): int { return PointFilter::KIND_METHOD | PointFilter::KIND_DYNAMIC; } } src/Aop/Pointcut/MatchInheritedPointcut.php000064400000003313144760133060015012 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; use Go\Aop\PointFilter; use ReflectionClass; use ReflectionMethod; use ReflectionProperty; /** * Pointcut that matches all inherited items, this is useful to filter inherited memebers via !matchInherited() */ class MatchInheritedPointcut implements Pointcut { use PointcutClassFilterTrait; /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { if (!$context instanceof ReflectionClass) { return false; } $isPointMethod = $point instanceof ReflectionMethod; $isPointProperty = $point instanceof ReflectionProperty; if (!$isPointMethod && !$isPointProperty) { return false; } $declaringClassName = $point->getDeclaringClass()->name; return $context->name !== $declaringClassName && $context->isSubclassOf($declaringClassName); } /** * Returns the kind of point filter */ public function getKind(): int { return PointFilter::KIND_METHOD | PointFilter::KIND_PROPERTY; } } src/Aop/Pointcut/NotPointcut.php000064400000003331144760133060012662 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; /** * Signature method pointcut checks method signature (modifiers and name) to match it */ class NotPointcut implements Pointcut { use PointcutClassFilterTrait; /** * Pointcut to invert */ protected $pointcut; /** * Kind of pointcut */ protected $kind = 0; /** * Inverse pointcut matcher */ public function __construct(Pointcut $pointcut) { $this->pointcut = $pointcut; $this->kind = $pointcut->getKind(); } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { $isMatchesPre = $this->pointcut->getClassFilter()->matches($context); if (!$isMatchesPre) { return true; } $isMatchesPoint = $this->pointcut->matches($point, $context, $instance, $arguments); if (!$isMatchesPoint) { return true; } return false; } /** * Returns the kind of point filter */ public function getKind(): int { return $this->kind; } } src/Aop/Pointcut/OrPointcut.php000064400000004425144760133060012507 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; use Go\Aop\PointFilter; use Go\Aop\Support\OrPointFilter; /** * Logical "OR" pointcut that combines two simple pointcuts */ class OrPointcut extends AndPointcut { /** * "Or" pointcut constructor */ public function __construct(Pointcut $first, Pointcut $second) { $this->first = $first; $this->second = $second; $this->kind = $first->getKind() | $second->getKind(); $this->classFilter = new OrPointFilter($first->getClassFilter(), $second->getClassFilter()); } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { return $this->matchPart($this->first, $point, $context, $instance, $arguments) || $this->matchPart($this->second, $point, $context, $instance, $arguments); } /** * @inheritDoc */ protected function matchPart( Pointcut $pointcut, $point, $context = null, $instance = null, array $arguments = null ): bool { $pointcutKind = $pointcut->getKind(); // We need to recheck filter kind one more time, because of OR syntax switch (true) { case ($point instanceof \ReflectionMethod && ($pointcutKind & PointFilter::KIND_METHOD)): case ($point instanceof \ReflectionProperty && ($pointcutKind & PointFilter::KIND_PROPERTY)): case ($point instanceof \ReflectionClass && ($pointcutKind & PointFilter::KIND_CLASS)): return parent::matchPart($pointcut, $point, $context, $instance, $arguments); default: return false; } } } src/Aop/Pointcut/PointcutClassFilterTrait.php000064400000002110144760133060015333 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\PointFilter; use Go\Aop\Support\TruePointFilter; /** * Convenient trait for pointcuts with class filter. * * The "classFilter" property can be set to customize ClassFilter behavior. */ trait PointcutClassFilterTrait { /** * Filter for class * * @var null|PointFilter */ protected $classFilter; /** * Set the ClassFilter to use for this pointcut. */ public function setClassFilter(PointFilter $classFilter): void { $this->classFilter = $classFilter; } /** * Return the class filter for this pointcut. */ public function getClassFilter(): PointFilter { if ($this->classFilter === null) { $this->classFilter = TruePointFilter::getInstance(); } return $this->classFilter; } } src/Aop/Pointcut/PointcutGrammar.php000064400000033655144760133060013524 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Closure; use Dissect\Lexer\Token; use Dissect\Parser\Grammar; use Doctrine\Common\Annotations\Reader; use Go\Aop\PointFilter; use Go\Aop\Support\AndPointFilter; use Go\Aop\Support\InheritanceClassFilter; use Go\Aop\Support\ModifierMatcherFilter; use Go\Aop\Support\ReturnTypeFilter; use Go\Aop\Support\SimpleNamespaceFilter; use Go\Aop\Support\TruePointFilter; use Go\Core\AspectContainer; use function constant; /** * Pointcut grammar defines general structure of pointcuts and rules of parsing */ class PointcutGrammar extends Grammar { /** * Constructs a pointcut grammar with AST */ public function __construct(AspectContainer $container, Reader $annotationReader) { $this('empty') ->is(/* empty */); $stringConverter = $this->getNodeToStringConverter(); $this('pointcutExpression') ->is('pointcutExpression', '||', 'conjugatedExpression') ->call(function ($first, $_0, $second) { return new OrPointcut($first, $second); }) ->is('conjugatedExpression'); $this('conjugatedExpression') ->is('conjugatedExpression', '&&', 'negatedExpression') ->call(function ($first, $_0, $second) { return new AndPointcut($first, $second); }) ->is('negatedExpression'); $this('negatedExpression') ->is('!', 'brakedExpression') ->call(function ($_0, $item) { return new NotPointcut($item); }) ->is('brakedExpression'); $this('brakedExpression') ->is('(', 'pointcutExpression', ')') ->call(function ($_0, $e, $_1) { return $e; }) ->is('singlePointcut'); $this('singlePointcut') ->is('accessPointcut') ->is('annotatedAccessPointcut') ->is('executionPointcut') ->is('annotatedExecutionPointcut') ->is('withinPointcut') ->is('annotatedWithinPointcut') ->is('initializationPointcut') ->is('staticInitializationPointcut') ->is('cflowbelowPointcut') ->is('dynamicExecutionPointcut') ->is('matchInheritedPointcut') ->is('pointcutReference'); $this('accessPointcut') ->is('access', '(', 'propertyAccessReference', ')') ->call(function ($_0, $_1, $propertyReference) { return $propertyReference; }); $this('executionPointcut') ->is('execution', '(', 'methodExecutionReference', ')') ->call(function ($_0, $_1, $methodReference) { return $methodReference; }) ->is('execution', '(', 'functionExecutionReference', ')') ->call(function ($_0, $_1, $functionReference) { return $functionReference; }); $this('withinPointcut') ->is('within', '(', 'classFilter', ')') ->call(function ($_0, $_1, $classFilter) { $pointcut = new TruePointcut(PointFilter::KIND_ALL); $pointcut->setClassFilter($classFilter); return $pointcut; }); $this('annotatedAccessPointcut') ->is('annotation', 'access', '(', 'namespaceName', ')') ->call(function ($_0, $_1, $_2, $annotationClassName) use ($annotationReader) { $kindProperty = PointFilter::KIND_PROPERTY; return new AnnotationPointcut($kindProperty, $annotationReader, $annotationClassName); }); $this('annotatedExecutionPointcut') ->is('annotation', 'execution', '(', 'namespaceName', ')') ->call(function ($_0, $_1, $_2, $annotationClassName) use ($annotationReader) { $kindMethod = PointFilter::KIND_METHOD; return new AnnotationPointcut($kindMethod, $annotationReader, $annotationClassName); }); $this('annotatedWithinPointcut') ->is('annotation', 'within', '(', 'namespaceName', ')') ->call(function ($_0, $_1, $_2, $annotationClassName) use ($annotationReader) { $pointcut = new TruePointcut(PointFilter::KIND_ALL); $kindClass = PointFilter::KIND_CLASS; $classFilter = new AnnotationPointcut($kindClass, $annotationReader, $annotationClassName); $pointcut->setClassFilter($classFilter); return $pointcut; }); $this('initializationPointcut') ->is('initialization', '(', 'classFilter', ')') ->call(function ($_0, $_1, $classFilter) { $pointcut = new TruePointcut(PointFilter::KIND_INIT + PointFilter::KIND_CLASS); $pointcut->setClassFilter($classFilter); return $pointcut; }); $this('staticInitializationPointcut') ->is('staticinitialization', '(', 'classFilter', ')') ->call(function ($_0, $_1, $classFilter) { $pointcut = new TruePointcut(PointFilter::KIND_STATIC_INIT + PointFilter::KIND_CLASS); $pointcut->setClassFilter($classFilter); return $pointcut; }); $this('cflowbelowPointcut') ->is('cflowbelow', '(', 'executionPointcut', ')') ->call(function ($_0, $_1, $pointcut) { return new CFlowBelowMethodPointcut($pointcut); }); $this('matchInheritedPointcut') ->is('matchInherited', '(', ')') ->call(function () { return new MatchInheritedPointcut(); }); $this('dynamicExecutionPointcut') // ideally, this should be 'dynamic', 'methodExecutionReference' ->is('dynamic', '(', 'memberReference', '(', 'argumentList', ')', ')') ->call(function ($_0, $_1, ClassMemberReference $reference) { $memberFilter = new AndPointFilter( $reference->getVisibilityFilter(), $reference->getAccessTypeFilter() ); $pointcut = new MagicMethodPointcut($reference->getMemberNamePattern(), $memberFilter); $pointcut->setClassFilter($reference->getClassFilter()); return $pointcut; }); $this('pointcutReference') ->is('namespaceName', '->', 'namePatternPart') ->call(function ($className, $_0, $name) use ($container) { return new PointcutReference($container, "{$className}->{$name}"); }); $this('propertyAccessReference') ->is('memberReference') ->call(function (ClassMemberReference $reference) { $memberFilter = new AndPointFilter( $reference->getVisibilityFilter(), $reference->getAccessTypeFilter() ); $pointcut = new SignaturePointcut( PointFilter::KIND_PROPERTY, $reference->getMemberNamePattern(), $memberFilter); $pointcut->setClassFilter($reference->getClassFilter()); return $pointcut; }); $this('methodExecutionReference') ->is('memberReference', '(', 'argumentList', ')') ->call(function (ClassMemberReference $reference) { $memberFilter = new AndPointFilter( $reference->getVisibilityFilter(), $reference->getAccessTypeFilter() ); $pointcut = new SignaturePointcut( PointFilter::KIND_METHOD, $reference->getMemberNamePattern(), $memberFilter ); $pointcut->setClassFilter($reference->getClassFilter()); return $pointcut; }) ->is('memberReference', '(', 'argumentList', ')', ':', 'namespaceName') ->call(function (ClassMemberReference $reference, $_0, $_1, $_2, $_3, $returnType) { $memberFilter = new AndPointFilter( $reference->getVisibilityFilter(), $reference->getAccessTypeFilter(), new ReturnTypeFilter($returnType) ); $pointcut = new SignaturePointcut( PointFilter::KIND_METHOD, $reference->getMemberNamePattern(), $memberFilter ); $pointcut->setClassFilter($reference->getClassFilter()); return $pointcut; }); $this('functionExecutionReference') ->is('namespacePattern', 'nsSeparator', 'namePatternPart', '(', 'argumentList', ')') ->call(function ($namespacePattern, $_0, $namePattern) { $nsFilter = new SimpleNamespaceFilter($namespacePattern); $pointcut = new FunctionPointcut($namePattern); $pointcut->setNamespaceFilter($nsFilter); return $pointcut; }) ->is('namespacePattern', 'nsSeparator', 'namePatternPart', '(', 'argumentList', ')', ':', 'namespaceName') ->call(function ($namespacePattern, $_0, $namePattern, $_1, $_2, $_3, $_4, $returnType) { $nsFilter = new SimpleNamespaceFilter($namespacePattern); $typeFilter = new ReturnTypeFilter($returnType); $pointcut = new FunctionPointcut($namePattern, $typeFilter); $pointcut->setNamespaceFilter($nsFilter); return $pointcut; }); $this('memberReference') ->is('memberModifiers', 'classFilter', 'memberAccessType', 'namePatternPart') ->call(function ( ModifierMatcherFilter $memberModifiers, PointFilter $classFilter, ModifierMatcherFilter $memberAccessType, $namePattern ) { $reference = new ClassMemberReference( $classFilter, $memberModifiers, $memberAccessType, $namePattern ); return $reference; }); $this('classFilter') ->is('namespacePattern') ->call(function ($pattern) { $truePointFilter = TruePointFilter::getInstance(); return $pattern === '**' ? $truePointFilter : new SignaturePointcut(PointFilter::KIND_CLASS, $pattern, $truePointFilter); }) ->is('namespacePattern', '+') ->call(function ($parentClassName) { return new InheritanceClassFilter($parentClassName); }); $this('argumentList') ->is('*'); $this('memberAccessType') ->is('::') ->call(function () { return new ModifierMatcherFilter(\ReflectionMethod::IS_STATIC); }) ->is('->') ->call(function () { $modifierMatcherFilter = new ModifierMatcherFilter(); $modifierMatcherFilter->notMatch(\ReflectionMethod::IS_STATIC); return $modifierMatcherFilter; }); $this('namespacePattern') ->is('**')->call($stringConverter) ->is('namePatternPart') ->is('namespacePattern', 'nsSeparator', 'namePatternPart')->call($stringConverter) ->is('namespacePattern', 'nsSeparator', '**')->call($stringConverter); $this('namePatternPart') ->is('*')->call($stringConverter) ->is('namePart')->call($stringConverter) ->is('namePatternPart', '*')->call($stringConverter) ->is('namePatternPart', 'namePart')->call($stringConverter) ->is('namePatternPart', '|', 'namePart')->call($stringConverter); $this('namespaceName') ->is('namePart')->call($stringConverter) ->is('namespaceName', 'nsSeparator', 'namePart')->call($stringConverter); $this('memberModifiers') ->is('memberModifier', '|', 'memberModifiers') ->call(function ($modifier, $_0, ModifierMatcherFilter $matcher) { return $matcher->orMatch($modifier); }) ->is('memberModifier', 'memberModifiers') ->call(function ($modifier, ModifierMatcherFilter $matcher) { return $matcher->andMatch($modifier); }) ->is('memberModifier') ->call(function ($modifier) { return new ModifierMatcherFilter($modifier); }); $converter = $this->getModifierConverter(); $this('memberModifier') ->is('public')->call($converter) ->is('protected')->call($converter) ->is('private')->call($converter) ->is('final')->call($converter); $this->start('pointcutExpression'); } /** * Returns callable for converting node(s) to the string */ private function getNodeToStringConverter(): Closure { return function (...$nodes) { $value = ''; foreach ($nodes as $node) { if (is_scalar($node)) { $value .= $node; } else { $value .= $node->getValue(); } } return $value; }; } /** * Returns callable for converting node value for modifiers to the constant value */ private function getModifierConverter(): Closure { return function (Token $token) { $name = strtoupper($token->getValue()); return constant("ReflectionMethod::IS_{$name}"); }; } } src/Aop/Pointcut/PointcutLexer.php000064400000003557144760133060013213 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Dissect\Lexer\SimpleLexer; /** * This class defines a lexer for pointcut expression */ class PointcutLexer extends SimpleLexer { /** * Lexer token definitions */ public function __construct() { // General tokens $this->token('execution'); $this->token('dynamic'); $this->token('within'); $this->token('access'); $this->token('cflowbelow'); $this->token('initialization'); $this->token('staticinitialization'); $this->token('matchInherited'); // Parenthesis $this->token('('); $this->token(')'); // Member modifiers $this->token('public'); $this->token('protected'); $this->token('private'); $this->token('final'); // Access type (dynamic or static) $this->token('->'); $this->token('::'); // Logic tokens $this->token('!'); $this->token('&'); $this->token('&&'); $this->token('|'); $this->token('||'); $this->token('annotation', '@'); // Regex for class name $this->regex('namePart', '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/'); // NS separator $this->token('nsSeparator', '\\'); // Return-type specifier $this->token(':'); // Special wildcard tokens $this->token('+'); $this->token('*'); $this->token('**'); // White spaces $this->regex('WSP', "/^[ \r\n\t]+/"); // Comments $this->regex('CMT', '|^//.*|'); $this->skip('CMT', 'WSP'); } } src/Aop/Pointcut/PointcutParseTable.php000064400000026203144760133060014147 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ /** * This table was generated for production use, do not touch it */ return ['action' => [0 => ['!' =>4, '(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 1 => ['||' =>31, '$eof' =>0,], 2 => ['&&' =>32, '$eof' =>-3, '||' =>-3, ')' =>-3,], 4 => ['(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 6 => ['!' =>4, '(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 20 => ['(' =>35,], 21 => ['access' =>36, 'execution' =>37, 'within' =>38,], 22 => ['(' =>39,], 23 => ['(' =>40,], 24 => ['(' =>41,], 25 => ['(' =>42,], 26 => ['(' =>43,], 27 => ['(' =>44,], 28 => ['(' =>45,], 29 => ['->' =>46, 'nsSeparator' =>47,], 31 => ['!' =>4, '(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 32 => ['!' =>4, '(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 34 => [')' =>50, '||' =>31,], 35 => ['public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58,], 36 => ['(' =>59,], 37 => ['(' =>60,], 38 => ['(' =>61,], 39 => ['**' =>66, '*' =>68, 'namePart' =>69, 'public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58,], 40 => ['**' =>66, '*' =>68, 'namePart' =>69,], 41 => ['**' =>66, '*' =>68, 'namePart' =>69,], 42 => ['**' =>66, '*' =>68, 'namePart' =>69,], 43 => ['execution' =>22,], 44 => ['public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58,], 45 => [')' =>76,], 46 => ['*' =>68, 'namePart' =>69,], 47 => ['namePart' =>78,], 48 => ['&&' =>32, '$eof' =>-2, '||' =>-2, ')' =>-2,], 51 => [')' =>79,], 53 => ['**' =>66, '*' =>68, 'namePart' =>69,], 54 => ['|' =>81, 'public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58, '**' =>-59, '*' =>-59, 'namePart' =>-59,], 59 => ['namePart' =>30,], 60 => ['namePart' =>30,], 61 => ['namePart' =>30,], 62 => [')' =>86,], 63 => [')' =>87,], 64 => ['(' =>88,], 65 => ['nsSeparator' =>89,], 67 => ['*' =>90, 'namePart' =>91, '|' =>92, 'nsSeparator' =>-47, ')' =>-47, '+' =>-47, '::' =>-47, '->' =>-47,], 70 => [')' =>93,], 71 => ['+' =>94, 'nsSeparator' =>95, ')' =>-41, '::' =>-41, '->' =>-41,], 72 => [')' =>96,], 73 => [')' =>97,], 74 => [')' =>98,], 75 => ['(' =>99,], 77 => ['*' =>90, 'namePart' =>91, '|' =>92, '$eof' =>-34, '||' =>-34, '&&' =>-34, ')' =>-34,], 80 => ['::' =>101, '->' =>102,], 81 => ['public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58,], 83 => [')' =>104, 'nsSeparator' =>47,], 84 => [')' =>105, 'nsSeparator' =>47,], 85 => [')' =>106, 'nsSeparator' =>47,], 88 => ['*' =>108,], 89 => ['**' =>110, '*' =>68, 'namePart' =>69,], 92 => ['namePart' =>111,], 95 => ['**' =>110, '*' =>68, 'namePart' =>69,], 99 => ['*' =>108,], 100 => ['*' =>68, 'namePart' =>69,], 107 => [')' =>115,], 109 => ['(' =>116, '*' =>90, 'namePart' =>91, '|' =>92, 'nsSeparator' =>-48,], 112 => ['*' =>90, 'namePart' =>91, '|' =>92, ')' =>-48, '+' =>-48, 'nsSeparator' =>-48, '::' =>-48, '->' =>-48,], 113 => [')' =>117,], 114 => ['*' =>90, 'namePart' =>91, '|' =>92, ')' =>-40, '(' =>-40,], 115 => [':' =>118, ')' =>-36,], 116 => ['*' =>108,], 117 => [')' =>120,], 118 => ['namePart' =>30,], 119 => [')' =>122,], 121 => ['nsSeparator' =>47, ')' =>-37,], 122 => [':' =>123, ')' =>-38,], 123 => ['namePart' =>30,], 124 => ['nsSeparator' =>47, ')' =>-39,], 3 => ['$eof' =>-5, '||' =>-5, '&&' =>-5, ')' =>-5,], 5 => ['$eof' =>-7, '||' =>-7, '&&' =>-7, ')' =>-7,], 7 => ['$eof' =>-9, '||' =>-9, '&&' =>-9, ')' =>-9,], 8 => ['$eof' =>-10, '||' =>-10, '&&' =>-10, ')' =>-10,], 9 => ['$eof' =>-11, '||' =>-11, '&&' =>-11, ')' =>-11,], 10 => ['$eof' =>-12, '||' =>-12, '&&' =>-12, ')' =>-12,], 11 => ['$eof' =>-13, '||' =>-13, '&&' =>-13, ')' =>-13,], 12 => ['$eof' =>-14, '||' =>-14, '&&' =>-14, ')' =>-14,], 13 => ['$eof' =>-15, '||' =>-15, '&&' =>-15, ')' =>-15,], 14 => ['$eof' =>-16, '||' =>-16, '&&' =>-16, ')' =>-16,], 15 => ['$eof' =>-17, '||' =>-17, '&&' =>-17, ')' =>-17,], 16 => ['$eof' =>-18, '||' =>-18, '&&' =>-18, ')' =>-18,], 17 => ['$eof' =>-19, '||' =>-19, '&&' =>-19, ')' =>-19,], 18 => ['$eof' =>-20, '||' =>-20, '&&' =>-20, ')' =>-20,], 19 => ['$eof' =>-21, '||' =>-21, '&&' =>-21, ')' =>-21,], 30 => ['->' =>-55, 'nsSeparator' =>-55, ')' =>-55,], 33 => ['$eof' =>-6, '||' =>-6, '&&' =>-6, ')' =>-6,], 49 => ['$eof' =>-4, '||' =>-4, '&&' =>-4, ')' =>-4,], 50 => ['$eof' =>-8, '||' =>-8, '&&' =>-8, ')' =>-8,], 52 => [')' =>-35,], 55 => ['**' =>-60, '*' =>-60, 'namePart' =>-60, '|' =>-60, 'public' =>-60, 'protected' =>-60, 'private' =>-60, 'final' =>-60,], 56 => ['**' =>-61, '*' =>-61, 'namePart' =>-61, '|' =>-61, 'public' =>-61, 'protected' =>-61, 'private' =>-61, 'final' =>-61,], 57 => ['**' =>-62, '*' =>-62, 'namePart' =>-62, '|' =>-62, 'public' =>-62, 'protected' =>-62, 'private' =>-62, 'final' =>-62,], 58 => ['**' =>-63, '*' =>-63, 'namePart' =>-63, '|' =>-63, 'public' =>-63, 'protected' =>-63, 'private' =>-63, 'final' =>-63,], 66 => ['nsSeparator' =>-46, ')' =>-46, '+' =>-46, '::' =>-46, '->' =>-46,], 68 => ['$eof' =>-50, '||' =>-50, '&&' =>-50, ')' =>-50, '(' =>-50, 'nsSeparator' =>-50, '*' =>-50, 'namePart' =>-50, '|' =>-50, '+' =>-50, '::' =>-50, '->' =>-50,], 69 => ['$eof' =>-51, '||' =>-51, '&&' =>-51, ')' =>-51, '(' =>-51, 'nsSeparator' =>-51, '*' =>-51, 'namePart' =>-51, '|' =>-51, '+' =>-51, '::' =>-51, '->' =>-51,], 76 => ['$eof' =>-32, '||' =>-32, '&&' =>-32, ')' =>-32,], 78 => ['->' =>-56, 'nsSeparator' =>-56, ')' =>-56,], 79 => ['$eof' =>-22, '||' =>-22, '&&' =>-22, ')' =>-22,], 82 => ['**' =>-58, '*' =>-58, 'namePart' =>-58,], 86 => ['$eof' =>-23, '||' =>-23, '&&' =>-23, ')' =>-23,], 87 => ['$eof' =>-24, '||' =>-24, '&&' =>-24, ')' =>-24,], 90 => ['$eof' =>-52, '||' =>-52, '&&' =>-52, ')' =>-52, '(' =>-52, 'nsSeparator' =>-52, '*' =>-52, 'namePart' =>-52, '|' =>-52, '+' =>-52, '::' =>-52, '->' =>-52,], 91 => ['$eof' =>-53, '||' =>-53, '&&' =>-53, ')' =>-53, '(' =>-53, 'nsSeparator' =>-53, '*' =>-53, 'namePart' =>-53, '|' =>-53, '+' =>-53, '::' =>-53, '->' =>-53,], 93 => ['$eof' =>-25, '||' =>-25, '&&' =>-25, ')' =>-25,], 94 => [')' =>-42, '::' =>-42, '->' =>-42,], 96 => ['$eof' =>-29, '||' =>-29, '&&' =>-29, ')' =>-29,], 97 => ['$eof' =>-30, '||' =>-30, '&&' =>-30, ')' =>-30,], 98 => ['$eof' =>-31, '||' =>-31, '&&' =>-31, ')' =>-31,], 101 => ['*' =>-44, 'namePart' =>-44,], 102 => ['*' =>-45, 'namePart' =>-45,], 103 => ['**' =>-57, '*' =>-57, 'namePart' =>-57,], 104 => ['$eof' =>-26, '||' =>-26, '&&' =>-26, ')' =>-26,], 105 => ['$eof' =>-27, '||' =>-27, '&&' =>-27, ')' =>-27,], 106 => ['$eof' =>-28, '||' =>-28, '&&' =>-28, ')' =>-28,], 108 => [')' =>-43,], 110 => ['nsSeparator' =>-49, ')' =>-49, '+' =>-49, '::' =>-49, '->' =>-49,], 111 => ['$eof' =>-54, '||' =>-54, '&&' =>-54, ')' =>-54, '(' =>-54, 'nsSeparator' =>-54, '*' =>-54, 'namePart' =>-54, '|' =>-54, '+' =>-54, '::' =>-54, '->' =>-54,], 120 => ['$eof' =>-33, '||' =>-33, '&&' =>-33, ')' =>-33,],], 'goto' => [0 => ['pointcutExpression' =>1, 'conjugatedExpression' =>2, 'negatedExpression' =>3, 'brakedExpression' =>5, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 4 => ['brakedExpression' =>33, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 6 => ['pointcutExpression' =>34, 'conjugatedExpression' =>2, 'negatedExpression' =>3, 'brakedExpression' =>5, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 31 => ['conjugatedExpression' =>48, 'negatedExpression' =>3, 'brakedExpression' =>5, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 32 => ['negatedExpression' =>49, 'brakedExpression' =>5, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 35 => ['propertyAccessReference' =>51, 'memberReference' =>52, 'memberModifiers' =>53, 'memberModifier' =>54,], 39 => ['methodExecutionReference' =>62, 'functionExecutionReference' =>63, 'memberReference' =>64, 'namespacePattern' =>65, 'memberModifiers' =>53, 'namePatternPart' =>67, 'memberModifier' =>54,], 40 => ['classFilter' =>70, 'namespacePattern' =>71, 'namePatternPart' =>67,], 41 => ['classFilter' =>72, 'namespacePattern' =>71, 'namePatternPart' =>67,], 42 => ['classFilter' =>73, 'namespacePattern' =>71, 'namePatternPart' =>67,], 43 => ['executionPointcut' =>74,], 44 => ['memberReference' =>75, 'memberModifiers' =>53, 'memberModifier' =>54,], 46 => ['namePatternPart' =>77,], 53 => ['classFilter' =>80, 'namespacePattern' =>71, 'namePatternPart' =>67,], 54 => ['memberModifiers' =>82, 'memberModifier' =>54,], 59 => ['namespaceName' =>83,], 60 => ['namespaceName' =>84,], 61 => ['namespaceName' =>85,], 80 => ['memberAccessType' =>100,], 81 => ['memberModifiers' =>103, 'memberModifier' =>54,], 88 => ['argumentList' =>107,], 89 => ['namePatternPart' =>109,], 95 => ['namePatternPart' =>112,], 99 => ['argumentList' =>113,], 100 => ['namePatternPart' =>114,], 116 => ['argumentList' =>119,], 118 => ['namespaceName' =>121,], 123 => ['namespaceName' =>124,],]]; src/Aop/Pointcut/PointcutParser.php000064400000001254144760133060013360 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Dissect\Parser\LALR1\Parser; /** * Pointcut parser extends the default parser with parse table and strict typehint for grammar */ class PointcutParser extends Parser { /** * {@inheritDoc} */ public function __construct(PointcutGrammar $grammar) { $parseTable = include __DIR__ . '/PointcutParseTable.php'; parent::__construct($grammar, $parseTable); } } src/Aop/Pointcut/PointcutReference.php000064400000004601144760133060014021 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; use Go\Aop\PointFilter; use Go\Core\AspectContainer; use Go\Core\AspectKernel; /** * Reference to the pointcut holds an id of pointcut to fetch when needed */ final class PointcutReference implements Pointcut { /** * @var Pointcut */ private $pointcut; /** * Name of the pointcut to fetch from the container */ private $pointcutId; /** * Instance of aspect container */ private $container; /** * Pointcut reference constructor */ public function __construct(AspectContainer $container, string $pointcutId) { $this->container = $container; $this->pointcutId = $pointcutId; } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { return $this->getPointcut()->matches($point, $context, $instance, $arguments); } /** * Returns the kind of point filter */ public function getKind(): int { return $this->getPointcut()->getKind(); } /** * Return the class filter for this pointcut. */ public function getClassFilter(): PointFilter { return $this->getPointcut()->getClassFilter(); } /** * @inheritdoc */ public function __sleep() { return ['pointcutId']; } /** * @inheritdoc */ public function __wakeup() { $this->container = AspectKernel::getInstance()->getContainer(); } /** * Returns a real pointcut from the container */ private function getPointcut(): Pointcut { if (!$this->pointcut) { $this->pointcut = $this->container->getPointcut($this->pointcutId); } return $this->pointcut; } } src/Aop/Pointcut/SignaturePointcut.php000064400000004212144760133060014062 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; use Go\Aop\PointFilter; /** * Signature pointcut checks element signature (modifiers and name) to match it */ class SignaturePointcut implements Pointcut { use PointcutClassFilterTrait; /** * Element name to match, can contain wildcards **,*,?,| */ protected $name = ''; /** * Regular expression for pattern matching */ protected $regexp; /** * Modifier filter for element */ protected $modifierFilter; /** * Filter kind, e.g. self::KIND_CLASS */ protected $filterKind = 0; /** * Signature matcher constructor */ public function __construct(int $filterKind, string $name, PointFilter $modifierFilter) { $this->filterKind = $filterKind; $this->name = $name; $this->regexp = strtr(preg_quote($this->name, '/'), [ '\\*' => '[^\\\\]+?', '\\*\\*' => '.+?', '\\?' => '.', '\\|' => '|' ]); $this->modifierFilter = $modifierFilter; } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { if (!$this->modifierFilter->matches($point, $context)) { return false; } return ($point->name === $this->name) || (bool) preg_match("/^(?:{$this->regexp})$/", $point->name); } /** * Returns the kind of point filter */ public function getKind(): int { return $this->filterKind; } } src/Aop/Pointcut/TruePointcut.php000064400000002517144760133060013046 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; /** * Canonical Pointcut instance that always matches. */ class TruePointcut implements Pointcut { use PointcutClassFilterTrait; /** * Filter kind */ protected $filterKind; /** * Default constructor can be used to specify concrete filter kind */ public function __construct(int $filterKind = self::KIND_ALL) { $this->filterKind = $filterKind; } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { return true; } /** * Returns the kind of point filter */ public function getKind(): int { return $this->filterKind; } } src/Aop/PointcutAdvisor.php000064400000001154144760133060011725 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Super-interface for all Advisors that are driven by a pointcut. * * This covers nearly all advisors except introduction advisors, for which method-level matching doesn't apply. */ interface PointcutAdvisor extends Advisor { /** * Gets the Pointcut that drives this advisor. */ public function getPointcut(): Pointcut; } src/Aop/Proxy.php000064400000000661144760133060007713 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop; /** * Marker interface implemented by all AOP proxies. * * Used to detect whether or not objects are Go-generated proxies. * * @api */ interface Proxy { } src/Aop/Support/AbstractGenericAdvisor.php000064400000001454144760133060014637 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\Advice; use Go\Aop\Advisor; /** * Abstract generic Advisor that allows for any Advice to be configured. */ abstract class AbstractGenericAdvisor implements Advisor { /** * Instance of advice */ protected $advice; /** * Initializes an advisor with advice */ public function __construct(Advice $advice) { $this->advice = $advice; } /** * Returns an advice to apply */ public function getAdvice(): Advice { return $this->advice; } } src/Aop/Support/AndPointFilter.php000064400000003051144760133060013124 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; /** * Logical "and" filter. */ class AndPointFilter implements PointFilter { /** * Kind of filter */ private $kind = -1; /** * List of PointFilters to combine with "AND" */ private $filters; /** * And constructor */ public function __construct(PointFilter ...$filters) { foreach ($filters as $filter) { $this->kind &= $filter->getKind(); } $this->filters = $filters; } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { foreach ($this->filters as $filter) { if (!$filter->matches($point, $context)) { return false; } } return true; } /** * Returns the kind of point filter */ public function getKind(): int { return $this->kind; } } src/Aop/Support/AnnotatedReflectionMethod.php000064400000002617144760133060015342 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Doctrine\Common\Annotations\Reader; use Go\Core\AspectKernel; use ReflectionMethod; /** * Extended version of ReflectionMethod with annotation support */ class AnnotatedReflectionMethod extends ReflectionMethod implements AnnotationAccess { /** * Annotation reader * * @var Reader */ private static $annotationReader; /** * Gets concrete annotation by name or null if the requested annotation does not exist. */ public function getAnnotation(string $annotationName): ?object { return self::getReader()->getMethodAnnotation($this, $annotationName); } /** * Gets all annotations applied to the current item. */ public function getAnnotations(): array { return self::getReader()->getMethodAnnotations($this); } /** * Returns an annotation reader */ private static function getReader(): Reader { if (self::$annotationReader === null) { self::$annotationReader = AspectKernel::getInstance()->getContainer()->get('aspect.annotation.reader'); } return self::$annotationReader; } } src/Aop/Support/AnnotatedReflectionProperty.php000064400000002633144760133060015744 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Doctrine\Common\Annotations\Reader; use Go\Core\AspectKernel; use ReflectionProperty; /** * Extended version of ReflectionProperty with annotation support */ class AnnotatedReflectionProperty extends ReflectionProperty implements AnnotationAccess { /** * Annotation reader * * @var Reader */ private static $annotationReader; /** * Gets concrete annotation by name or null if the requested annotation does not exist. */ public function getAnnotation(string $annotationName): ?object { return self::getReader()->getPropertyAnnotation($this, $annotationName); } /** * Gets all annotations applied to the current item. */ public function getAnnotations(): array { return self::getReader()->getPropertyAnnotations($this); } /** * Returns an annotation reader */ private static function getReader(): Reader { if (self::$annotationReader === null) { self::$annotationReader = AspectKernel::getInstance()->getContainer()->get('aspect.annotation.reader'); } return self::$annotationReader; } } src/Aop/Support/AnnotationAccess.php000064400000001257144760133060013504 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; /** * Provides access to annotations from reflection class/property/method. */ interface AnnotationAccess { /** * Gets concrete annotation by name or null if the requested annotation does not exist. */ public function getAnnotation(string $annotationName): ?object; /** * Gets all annotations applied to the current item. */ public function getAnnotations(): array; } src/Aop/Support/DeclareParentsAdvisor.php000064400000001542144760133060014471 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\IntroductionAdvisor; use Go\Aop\IntroductionInfo; use Go\Aop\Pointcut\PointcutClassFilterTrait; use Go\Aop\PointFilter; /** * Introduction advisor delegating to the given object. */ class DeclareParentsAdvisor extends AbstractGenericAdvisor implements IntroductionAdvisor { use PointcutClassFilterTrait; /** * Creates an advisor for declaring mixins via trait and interface. */ public function __construct(PointFilter $classFilter, IntroductionInfo $info) { $this->classFilter = $classFilter; parent::__construct($info); } } src/Aop/Support/DefaultPointcutAdvisor.php000064400000003323144760133060014706 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\Advice; use Go\Aop\Framework\DynamicInvocationMatcherInterceptor; use Go\Aop\Intercept\Interceptor; use Go\Aop\Pointcut; use Go\Aop\PointcutAdvisor; use Go\Aop\PointFilter; /** * Convenient Pointcut-driven Advisor implementation. * * This is the most commonly used Advisor implementation. It can be used with any pointcut and advice type, * except for introductions. There is normally no need to subclass this class, or to implement custom Advisors. */ class DefaultPointcutAdvisor extends AbstractGenericAdvisor implements PointcutAdvisor { /** * The Pointcut targeting the Advice */ private $pointcut; /** * Creates a DefaultPointcutAdvisor, specifying the Advice to run when Pointcut matches */ public function __construct(Pointcut $pointcut, Advice $advice) { $this->pointcut = $pointcut; parent::__construct($advice); } /** * {@inheritdoc} */ public function getAdvice(): Advice { $advice = parent::getAdvice(); if (($advice instanceof Interceptor) && ($this->pointcut->getKind() & PointFilter::KIND_DYNAMIC)) { $advice = new DynamicInvocationMatcherInterceptor( $this->pointcut, $advice ); } return $advice; } /** * Get the Pointcut that drives this advisor. */ public function getPointcut(): Pointcut { return $this->pointcut; } } src/Aop/Support/InheritanceClassFilter.php000064400000003053144760133060014631 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use ReflectionClass; use Go\Aop\PointFilter; /** * Inheritance class matcher that match single class name or any subclass */ class InheritanceClassFilter implements PointFilter { /** * Parent class or interface name to match in hierarchy */ protected $parentClass; /** * Inheritance class matcher constructor */ public function __construct(string $parentClassName) { $this->parentClass = $parentClassName; } /** * Performs matching of point of code * * @param mixed $class Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($class, $context = null, $instance = null, array $arguments = null): bool { if (!$class instanceof ReflectionClass) { return false; } return $class->isSubclassOf($this->parentClass) || \in_array($this->parentClass, $class->getInterfaceNames()); } /** * Returns the kind of point filter */ public function getKind(): int { return self::KIND_CLASS; } } src/Aop/Support/LazyPointcutAdvisor.php000064400000003536144760133060014247 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\Advice; use Go\Aop\Pointcut; use Go\Aop\PointcutAdvisor; use Go\Core\AspectContainer; /** * Lazy pointcut advisor is used to create a delayed pointcut only when needed */ class LazyPointcutAdvisor extends AbstractGenericAdvisor implements PointcutAdvisor { /** * Pointcut expression represented with string */ private $pointcutExpression; /** * Instance of parsed pointcut * * @var Pointcut|null */ private $pointcut; /** * Instance of aspect container */ private $container; /** * Creates the LazyPointcutAdvisor by specifying textual pointcut expression and Advice to run when Pointcut matches. */ public function __construct(AspectContainer $container, string $pointcutExpression, Advice $advice) { $this->container = $container; $this->pointcutExpression = $pointcutExpression; parent::__construct($advice); } /** * Get the Pointcut that drives this advisor. */ public function getPointcut(): Pointcut { if ($this->pointcut === null) { // Inject these dependencies and make them lazy! /** @var Pointcut\PointcutLexer $lexer */ $lexer = $this->container->get('aspect.pointcut.lexer'); /** @var Pointcut\PointcutParser $parser */ $parser = $this->container->get('aspect.pointcut.parser'); $tokenStream = $lexer->lex($this->pointcutExpression); $this->pointcut = $parser->parse($tokenStream); } return $this->pointcut; } } src/Aop/Support/ModifierMatcherFilter.php000064400000004346144760133060014462 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; /** * ModifierMatcherFilter performs checks on modifiers for reflection point */ class ModifierMatcherFilter implements PointFilter { /** * Bit mask, that should be always match */ protected $andMask = 0; /** * Bit mask, that can be used for additional check */ protected $orMask = 0; /** * Bit mask to exclude specific value from matching, for example, !public */ protected $notMask = 0; /** * Initialize default filter with "and" mask * * @param int $initialMask Default mask for "and" */ public function __construct(int $initialMask = 0) { $this->andMask = $initialMask; } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { $modifiers = $point->getModifiers(); return !($this->notMask & $modifiers) && (($this->andMask === ($this->andMask & $modifiers)) || ($this->orMask & $modifiers)); } /** * Add "and" mask */ public function andMatch(int $bitMask): self { $this->andMask |= $bitMask; return $this; } /** * Add "or" mask */ public function orMatch(int $bitMask): self { $this->orMask |= $bitMask; return $this; } /** * Add "not" mask */ public function notMatch(int $bitMask): self { $this->notMask |= $bitMask; return $this; } /** * Returns the kind of point filter */ public function getKind(): int { return self::KIND_ALL; } } src/Aop/Support/NamespacedReflectionFunction.php000064400000001741144760133060016027 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use ReflectionFunction; /** * Namespaced version of global functions */ class NamespacedReflectionFunction extends ReflectionFunction { /** * Custom namespace name */ private $namespace; /** * Extends the logic with passing the namespace name * * {@inheritDoc} */ public function __construct(string $functionName, string $namespaceName = '') { $this->namespace = $namespaceName; parent::__construct($functionName); } /** * {@inheritDoc} */ public function getNamespaceName() { if (!empty($this->namespace)) { return $this->namespace; } return parent::getNamespaceName(); } } src/Aop/Support/NotPointFilter.php000064400000002543144760133060013167 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; /** * Logical "not" filter. */ class NotPointFilter implements PointFilter { /** * Kind of filter */ private $kind; /** * Instance of filter to negate */ private $filter; /** * Not constructor */ public function __construct(PointFilter $filter) { $this->kind = $filter->getKind(); $this->filter = $filter; } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { return !$this->filter->matches($point, $context); } /** * Returns the kind of point filter */ public function getKind(): int { return $this->kind; } } src/Aop/Support/OrPointFilter.php000064400000003030144760133060012777 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; /** * Logical "or" filter. */ class OrPointFilter implements PointFilter { /** * Kind of filter */ private $kind = 0; /** * List of PointFilter to combine */ private $filters; /** * Or constructor */ public function __construct(PointFilter ...$filters) { foreach ($filters as $filter) { $this->kind |= $filter->getKind(); } $this->filters = $filters; } /** * Performs matching of point of code * * @param mixed $point Specific part of code, can be any Reflection class * @param null|mixed $context Related context, can be class or namespace * @param null|string|object $instance Invocation instance or string for static calls * @param null|array $arguments Dynamic arguments for method */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { foreach ($this->filters as $filter) { if ($filter->matches($point, $context)) { return true; } } return false; } /** * Returns the kind of point filter */ public function getKind(): int { return $this->kind; } } src/Aop/Support/PointcutBuilder.php000064400000006462144760133060013367 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Closure; use Go\Aop\Advice; use Go\Aop\Framework\AfterInterceptor; use Go\Aop\Framework\AfterThrowingInterceptor; use Go\Aop\Framework\AroundInterceptor; use Go\Aop\Framework\BeforeInterceptor; use Go\Aop\Framework\DeclareErrorInterceptor; use Go\Core\AspectContainer; /** * Pointcut builder provides simple DSL for declaring pointcuts in plain PHP code */ class PointcutBuilder { /** * Instance of aspect container */ protected $container; /** * Default constructor for the builder */ public function __construct(AspectContainer $container) { $this->container = $container; } /** * Declares the "Before" hook for specific pointcut expression */ public function before(string $pointcutExpression, Closure $adviceToInvoke): void { $interceptor = new BeforeInterceptor($adviceToInvoke, 0, $pointcutExpression); $this->registerAdviceInContainer($pointcutExpression, $interceptor); } /** * Declares the "After" hook for specific pointcut expression */ public function after(string $pointcutExpression, Closure $adviceToInvoke): void { $interceptor = new AfterInterceptor($adviceToInvoke, 0, $pointcutExpression); $this->registerAdviceInContainer($pointcutExpression, $interceptor); } /** * Declares the "AfterThrowing" hook for specific pointcut expression */ public function afterThrowing(string $pointcutExpression, Closure $adviceToInvoke): void { $interceptor = new AfterThrowingInterceptor($adviceToInvoke, 0, $pointcutExpression); $this->registerAdviceInContainer($pointcutExpression, $interceptor); } /** * Declares the "Around" hook for specific pointcut expression */ public function around(string $pointcutExpression, Closure $adviceToInvoke): void { $interceptor = new AroundInterceptor($adviceToInvoke, 0, $pointcutExpression); $this->registerAdviceInContainer($pointcutExpression, $interceptor); } /** * Declares the error message for specific pointcut expression with concrete error level */ 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 */ private function registerAdviceInContainer(string $pointcutExpression, Advice $adviceToInvoke): void { $this->container->registerAdvisor( new LazyPointcutAdvisor($this->container, $pointcutExpression, $adviceToInvoke), $this->getPointcutId($pointcutExpression) ); } /** * Returns an unique name for given pointcut expression */ private function getPointcutId(string $pointcutExpression): string { static $index = 0; return preg_replace('/\W+/', '_', $pointcutExpression) . '.' . $index++; } } src/Aop/Support/ReturnTypeFilter.php000064400000003525144760133060013537 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; use ReflectionFunctionAbstract; /** * Return type filter matcher methods and function with specific return type * * Type name can contain wildcards '*', '**' and '?' */ class ReturnTypeFilter implements PointFilter { /** * Return type name to match, can contain wildcards *,? */ protected $typeName; /** * Pattern for regular expression matching */ protected $regexp; /** * Return type name matcher constructor accepts name or glob pattern of the type to match */ public function __construct(string $returnTypeName) { $returnTypeName = trim($returnTypeName, '\\'); $this->typeName = $returnTypeName; $this->regexp = strtr(preg_quote($this->typeName, '/'), [ '\\*' => '[^\\\\]+', '\\*\\*' => '.+', '\\?' => '.', '\\|' => '|' ]); } /** * {@inheritdoc} */ public function matches($functionLike, $context = null, $instance = null, array $arguments = null): bool { if (!$functionLike instanceof ReflectionFunctionAbstract) { return false; } if (!$functionLike->hasReturnType()) { return false; } $returnType = (string) $functionLike->getReturnType(); return ($returnType === $this->typeName) || (bool) preg_match("/^(?:{$this->regexp})$/", $returnType); } /** * Returns the kind of point filter */ public function getKind(): int { return self::KIND_METHOD | self::KIND_FUNCTION; } } src/Aop/Support/SimpleNamespaceFilter.php000064400000003435144760133060014464 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; use Go\ParserReflection\ReflectionFileNamespace; /** * Simple namespace matcher that match only specific namespace name * * Namespace name can contain wildcards '*', '**' and '?' */ class SimpleNamespaceFilter implements PointFilter { /** * Namespace name to match, can contain wildcards *,? */ protected $nsName; /** * Pattern for regular expression matching */ protected $regexp; /** * Namespace name matcher constructor that accepts name or glob pattern to match */ public function __construct(string $namespaceName) { $namespaceName = trim($namespaceName, '\\'); $this->nsName = $namespaceName; $this->regexp = strtr(preg_quote($this->nsName, '/'), [ '\\*' => '[^\\\\]+', '\\*\\*' => '.+', '\\?' => '.', '\\|' => '|' ]); } /** * {@inheritdoc} */ public function matches($ns, $context = null, $instance = null, array $arguments = null): bool { $isNamespaceIsObject = ($ns === (object) $ns); if ($isNamespaceIsObject && !$ns instanceof ReflectionFileNamespace) { return false; } $nsName = ($ns instanceof ReflectionFileNamespace) ? $ns->getName() : $ns; return ($nsName === $this->nsName) || (bool) preg_match("/^(?:{$this->regexp})$/", $nsName); } /** * Returns the kind of point filter */ public function getKind(): int { return 0; } } src/Aop/Support/TruePointFilter.php000064400000002031144760133060013336 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; /** * Canonical PointFilter instance that matches all points. */ class TruePointFilter implements PointFilter { /** * Private class constructor */ private function __construct() { } /** * Singleton pattern */ public static function getInstance(): self { static $instance; if (!$instance) { $instance = new self(); } return $instance; } /** * @inheritdoc */ public function matches($point, $context = null, $instance = null, array $arguments = null): bool { return true; } /** * Returns the kind of point filter */ public function getKind(): int { return self::KIND_ALL; } } src/Bridge/Doctrine/MetadataLoadInterceptor.php000064400000011544144760133060015557 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Bridge\Doctrine; use Doctrine\Common\EventSubscriber; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\Mapping\ClassMetadata; use Go\Core\AspectContainer; /** * Class MetadataLoadInterceptor * * Support for weaving Doctrine entities. */ final class MetadataLoadInterceptor implements EventSubscriber { /** * {@inheritdoc} */ public function getSubscribedEvents() { return [ Events::loadClassMetadata ]; } /** * Handles \Doctrine\ORM\Events::loadClassMetadata event by modifying metadata of Go! AOP proxied classes. * * This method intercepts loaded metadata of Doctrine's entities which are weaved by Go! AOP, * and denotes them as mapped superclass. If weaved entities uses mappings from traits * (such as Timestampable, Blameable, etc... from https://github.com/Atlantic18/DoctrineExtensions), * it will remove all mappings from proxied class for fields inherited from traits in order to prevent * collision with concrete subclass of weaved entity. Fields from trait will be present in concrete subclass * of weaved entitites. * * @see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses * @see https://github.com/Atlantic18/DoctrineExtensions */ public function loadClassMetadata(LoadClassMetadataEventArgs $args): void { /** * @var ClassMetadata $metadata */ $metadata = $args->getClassMetadata(); if (1 === preg_match(sprintf('/.+(%s)$/', AspectContainer::AOP_PROXIED_SUFFIX), $metadata->name)) { $metadata->isMappedSuperclass = true; $metadata->isEmbeddedClass = false; $metadata->table = []; $metadata->customRepositoryClassName = null; $this->removeMappingsFromTraits($metadata); } } /** * Remove fields in Go! AOP proxied class metadata that are inherited * from traits. */ private function removeMappingsFromTraits(ClassMetadata $metadata): void { $traits = $this->getTraits($metadata->name); foreach ($traits as $trait) { $trait = new \ReflectionClass($trait); /** * @var \ReflectionProperty $property */ foreach ($trait->getProperties() as $property) { $name = $property->getName(); if (isset($metadata->fieldMappings[$name])) { $mapping = $metadata->fieldMappings[$name]; unset( $metadata->fieldMappings[$name], $metadata->fieldNames[$mapping['columnName']], $metadata->columnNames[$name] ); } } } } /** * Get ALL traits used by one class. * * This method is copied from https://github.com/RunOpenCode/traitor-bundle/blob/master/src/RunOpenCode/Bundle/Traitor/Utils/ClassUtils.php * * @param object|string $objectOrClass Instance of class or FQCN * @param bool $autoload Weather to autoload class. * * @throws \InvalidArgumentException * @throws \RuntimeException */ private function getTraits($objectOrClass, $autoload = true): array { if (is_object($objectOrClass)) { $objectOrClass = get_class($objectOrClass); } if (!is_string($objectOrClass)) { throw new \InvalidArgumentException(sprintf('Full qualified class name expected, got: "%s".', gettype($objectOrClass))); } if (!class_exists($objectOrClass)) { throw new \RuntimeException(sprintf('Class "%s" does not exists or it can not be autoloaded.', $objectOrClass)); } $traits = []; // Get traits of all parent classes do { $traits = array_merge(class_uses($objectOrClass, $autoload), $traits); } while ($objectOrClass = get_parent_class($objectOrClass)); $traitsToSearch = $traits; while (count($traitsToSearch) > 0) { $newTraits = class_uses(array_pop($traitsToSearch), $autoload); $traits = array_merge($newTraits, $traits); $traitsToSearch = array_merge($newTraits, $traitsToSearch); } foreach ($traits as $trait => $same) { $traits = array_merge(class_uses($trait, $autoload), $traits); } return array_unique(array_map(function ($fqcn) { return ltrim($fqcn, '\\'); }, $traits)); } } src/Console/Command/BaseAspectCommand.php000064400000003635144760133060014350 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Console\Command; use Go\Core\AspectKernel; use InvalidArgumentException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Base command for all aspect commands * * @codeCoverageIgnore */ class BaseAspectCommand extends Command { /** * Stores an instance of aspect kernel * * @var null|AspectKernel */ protected $aspectKernel; /** * {@inheritDoc} */ protected function configure() { $this->addArgument('loader', InputArgument::REQUIRED, 'Path to the aspect loader file'); } /** * Loads aspect kernel. * * Aspect kernel is loaded by executing loader and fetching singleton instance. * If your application environment initializes aspect kernel differently, you may * modify this method to get aspect kernel suitable to your needs. */ protected function loadAspectKernel(InputInterface $input, OutputInterface $output): void { $loader = $input->getArgument('loader'); $path = stream_resolve_include_path($loader); if (!is_readable($path)) { throw new InvalidArgumentException("Invalid loader path: {$loader}"); } ob_start(); include_once $path; ob_clean(); if (!class_exists(AspectKernel::class, false)) { $message = "Kernel was not initialized yet, please configure it in the {$path}"; throw new InvalidArgumentException($message); } $this->aspectKernel = AspectKernel::getInstance(); } } src/Console/Command/CacheWarmupCommand.php000064400000002436144760133060014533 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Console\Command; use Go\Instrument\ClassLoading\CacheWarmer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Console command for warming the cache * * @codeCoverageIgnore */ class CacheWarmupCommand extends BaseAspectCommand { /** * {@inheritDoc} */ protected function configure() { parent::configure(); $this ->setName('cache:warmup:aop') ->setDescription('Warm up the cache with woven aspects') ->setHelp(<<loadAspectKernel($input, $output); $warmer = new CacheWarmer($this->aspectKernel, $output); $warmer->warmUp(); } } src/Console/Command/DebugAdvisorCommand.php000064400000012433144760133060014710 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Console\Command; use Go\Aop\Advisor; use Go\Core\AdviceMatcher; use Go\Core\AspectContainer; use Go\Core\AspectLoader; use Go\Instrument\FileSystem\Enumerator; use Go\ParserReflection\ReflectionFile; use ReflectionClass; use ReflectionProperty; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * Console command to debug an advisors * * @codeCoverageIgnore */ class DebugAdvisorCommand extends BaseAspectCommand { /** * {@inheritDoc} */ protected function configure() { parent::configure(); $this ->setName('debug:advisor') ->addOption('advisor', null, InputOption::VALUE_OPTIONAL, 'Identifier of advisor') ->setDescription('Provides an interface for checking and debugging advisors') ->setHelp(<<loadAspectKernel($input, $output); $io = new SymfonyStyle($input, $output); $io->title('Advisor debug information'); $advisorId = $input->getOption('advisor'); if (!$advisorId) { $this->showAdvisorsList($io); } else { $this->showAdvisorInformation($io, $advisorId); } } private function showAdvisorsList(SymfonyStyle $io): void { $io->writeln('List of registered advisors in the container'); $aspectContainer = $this->aspectKernel->getContainer(); $advisors = $this->loadAdvisorsList($aspectContainer); $tableRows = []; foreach ($advisors as $id => $advisor) { [,$id] = explode('.', $id, 2); $advice = $advisor->getAdvice(); $expression = ''; try { $pointcutExpression = new ReflectionProperty($advice, 'pointcutExpression'); $pointcutExpression->setAccessible(true); $expression = $pointcutExpression->getValue($advice); } catch (\ReflectionException $e) { // nothing here, just ignore } $tableRows[] = [$id, $expression]; } $io->table(['Id', 'Expression'], $tableRows); $io->writeln([ 'If you want to query an information about concrete advisor, then just query it', 'by adding --advisor="Advisor\\Name" to the command' ]); } private function showAdvisorInformation(SymfonyStyle $io, string $advisorId): void { $aspectContainer = $this->aspectKernel->getContainer(); /** @var AdviceMatcher $adviceMatcher */ $adviceMatcher = $aspectContainer->get('aspect.advice_matcher'); $this->loadAdvisorsList($aspectContainer); $advisor = $aspectContainer->getAdvisor($advisorId); $options = $this->aspectKernel->getOptions(); $enumerator = new Enumerator($options['appDir'], $options['includePaths'], $options['excludePaths']); $iterator = $enumerator->enumerate(); $totalFiles = iterator_count($iterator); $io->writeln("Total {$totalFiles} files to analyze."); $iterator->rewind(); foreach ($iterator as $file) { $reflectionFile = new ReflectionFile((string) $file); $reflectionNamespaces = $reflectionFile->getFileNamespaces(); foreach ($reflectionNamespaces as $reflectionNamespace) { foreach ($reflectionNamespace->getClasses() as $reflectionClass) { $advices = $adviceMatcher->getAdvicesForClass($reflectionClass, [$advisorId => $advisor]); if (!empty($advices)) { $this->writeInfoAboutAdvices($io, $reflectionClass, $advices); } } } } } private function writeInfoAboutAdvices(SymfonyStyle $io, ReflectionClass $reflectionClass, array $advices): void { $className = $reflectionClass->getName(); foreach ($advices as $type => $typedAdvices) { foreach ($typedAdvices as $pointName => $advice) { $io->writeln(" -> matching {$type} {$className}->{$pointName}"); } } } /** * Collects list of advisors from the given aspect container * * @return Advisor[] List of advisors in the container */ private function loadAdvisorsList(AspectContainer $aspectContainer): array { /** @var AspectLoader $aspectLoader */ $aspectLoader = $aspectContainer->get('aspect.cached.loader'); $aspects = $aspectLoader->getUnloadedAspects(); foreach ($aspects as $aspect) { $aspectLoader->loadAndRegister($aspect); } $advisors = $aspectContainer->getByTag('advisor'); return $advisors; } } src/Console/Command/DebugAspectCommand.php000064400000007706144760133060014527 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Console\Command; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Pointcut; use Go\Core\AspectLoader; use ReflectionObject; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * Console command for querying an information about aspects * * @codeCoverageIgnore */ class DebugAspectCommand extends BaseAspectCommand { /** * {@inheritDoc} */ protected function configure() { parent::configure(); $this ->setName('debug:aspect') ->addOption('aspect', null, InputOption::VALUE_OPTIONAL, 'Optional aspect name to filter') ->setDescription('Provides an interface for querying the information about aspects') ->setHelp(<<loadAspectKernel($input, $output); $io = new SymfonyStyle($input, $output); $container = $this->aspectKernel->getContainer(); $aspects = []; $io->title('Aspect debug information'); $aspectName = $input->getOption('aspect'); if (!$aspectName) { $io->text('' . get_class($this->aspectKernel) . ' has following enabled aspects:'); $aspects = $container->getByTag('aspect'); } else { $aspect = $container->getAspect($aspectName); $aspects[] = $aspect; } $this->showRegisteredAspectsInfo($io, $aspects); } /** * Shows an information about registered aspects * * @param Aspect[] $aspects List of aspects */ private function showRegisteredAspectsInfo(SymfonyStyle $io, array $aspects): void { foreach ($aspects as $aspect) { $this->showAspectInfo($io, $aspect); } } /** * Displays an information about single aspect */ private function showAspectInfo(SymfonyStyle $io, Aspect $aspect): void { $refAspect = new ReflectionObject($aspect); $aspectName = $refAspect->getName(); $io->section($aspectName); $io->writeln('Defined in: ' . $refAspect->getFileName() . ''); $docComment = $refAspect->getDocComment(); if ($docComment) { $io->writeln($this->getPrettyText($docComment)); } $this->showAspectPointcutsAndAdvisors($io, $aspect); } /** * Shows an information about aspect pointcuts and advisors */ private function showAspectPointcutsAndAdvisors(SymfonyStyle $io, Aspect $aspect): void { /** @var AspectLoader $aspectLoader */ $container = $this->aspectKernel->getContainer(); $aspectLoader = $container->get('aspect.loader'); $io->writeln('Pointcuts and advices'); $aspectItems = $aspectLoader->load($aspect); $aspectItemsInfo = []; foreach ($aspectItems as $itemId => $item) { $itemType = 'Unknown'; if ($item instanceof Pointcut) { $itemType = 'Pointcut'; } if ($item instanceof Advisor) { $itemType = 'Advisor'; } $aspectItemsInfo[] = [$itemType, $itemId]; } $io->table(['Type', 'Identifier'], $aspectItemsInfo); } /** * Gets the reformatted comment text. */ private function getPrettyText(string $comment): string { $text = preg_replace('|^\s*/?\*+/?|m', '', $comment); return $text; } } src/Console/Command/DebugWeavingCommand.php000064400000007106144760133060014702 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Console\Command; use FilesystemIterator; use Go\Instrument\ClassLoading\CachePathManager; use Go\Instrument\ClassLoading\CacheWarmer; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * Console command for debugging weaving issues due to circular dependencies. * * @codeCoverageIgnore */ class DebugWeavingCommand extends BaseAspectCommand { /** * {@inheritDoc} */ protected function configure() { parent::configure(); $this ->setName('debug:weaving') ->setDescription('Checks consistency in weaving process') ->setHelp(<<loadAspectKernel($input, $output); $io = new SymfonyStyle($input, $output); $io->title('Weaving debug information'); $cachePathManager = $this->aspectKernel->getContainer()->get('aspect.cache.path.manager'); $warmer = new CacheWarmer($this->aspectKernel, new NullOutput()); $warmer->warmUp(); $proxies = $this->getProxies($cachePathManager); $cachePathManager->clearCacheState(); $warmer->warmUp(); $errors = 0; foreach ($this->getProxies($cachePathManager) as $path => $content) { if (!isset($proxies[$path])) { $io->error(sprintf('Proxy on path "%s" is generated on second "warmup" pass.', $path)); $errors++; continue; } if (isset($proxies[$path]) && $proxies[$path] !== $content) { $io->error(sprintf('Proxy on path "%s" is weaved differnlty on second "warmup" pass.', $path)); $errors++; continue; } $io->note(sprintf('Proxy on path "%s" is consistently weaved.', $path)); } if ($errors > 0) { $io->error(sprintf('Weaving is unstable, there are %s reported error(s).', $errors)); return $errors; } $io->success('Weaving is stable, there are no errors reported.'); } /** * Gets Go! AOP generated proxy classes (paths and their contents) from the cache. */ private function getProxies(CachePathManager $cachePathManager): array { $path = $cachePathManager->getCacheDir() . '/_proxies'; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS ), RecursiveIteratorIterator::CHILD_FIRST ); $proxies = []; /** * @var \SplFileInfo $splFileInfo */ foreach ($iterator as $splFileInfo) { if ($splFileInfo->isFile()) { $proxies[$splFileInfo->getPathname()] = file_get_contents($splFileInfo->getPathname()); } } return $proxies; } } src/Core/AbstractAspectLoaderExtension.php000064400000010316144760133060014670 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Dissect\Lexer\Exception\RecognitionException; use Dissect\Lexer\Lexer; use Dissect\Lexer\TokenStream\TokenStream; use Dissect\Parser\Exception\UnexpectedTokenException; use Dissect\Parser\Parser; use Go\Aop\Aspect; use Go\Aop\Pointcut; use Go\Aop\PointFilter; use ReflectionMethod; use ReflectionProperty; use UnexpectedValueException; /** * Abstract aspect loader */ abstract class AbstractAspectLoaderExtension implements AspectLoaderExtension { /** * Instance of pointcut lexer */ protected $pointcutLexer; /** * Instance of pointcut parser */ protected $pointcutParser; /** * Default loader constructor that accepts pointcut lexer and parser */ public function __construct(Lexer $pointcutLexer, Parser $pointcutParser) { $this->pointcutLexer = $pointcutLexer; $this->pointcutParser = $pointcutParser; } /** * General method for parsing pointcuts * * @param mixed|\ReflectionMethod|\ReflectionProperty $reflection Reflection of point * * @throws \UnexpectedValueException if there was an error during parsing * @return Pointcut|PointFilter */ final protected function parsePointcut(Aspect $aspect, $reflection, string $pointcutExpression): PointFilter { $stream = $this->makeLexicalAnalyze($aspect, $reflection, $pointcutExpression); return $this->parseTokenStream($reflection, $pointcutExpression, $stream); } /** * Performs lexical analyze of pointcut * * @param ReflectionMethod|ReflectionProperty $reflection * * @throws UnexpectedValueException */ private function makeLexicalAnalyze(Aspect $aspect, $reflection, string $pointcutExpression): TokenStream { try { $resolvedThisPointcut = str_replace('$this', \get_class($aspect), $pointcutExpression); $stream = $this->pointcutLexer->lex($resolvedThisPointcut); } catch (RecognitionException $e) { $message = 'Can not recognize the lexical structure `%s` before %s, defined in %s:%d'; $message = sprintf( $message, $pointcutExpression, (isset($reflection->class) ? $reflection->class . '->' : '') . $reflection->name, method_exists($reflection, 'getFileName') ? $reflection->getFileName() : $reflection->getDeclaringClass()->getFileName(), method_exists($reflection, 'getStartLine') ? $reflection->getStartLine() : 0 ); throw new UnexpectedValueException($message, 0, $e); } return $stream; } /** * Performs parsing of pointcut * * @param ReflectionMethod|ReflectionProperty $reflection * * @throws UnexpectedValueException */ private function parseTokenStream($reflection, string $pointcutExpression, TokenStream $stream): PointFilter { try { $pointcut = $this->pointcutParser->parse($stream); } catch (UnexpectedTokenException $e) { $token = $e->getToken(); $message = 'Unexpected token %s in the `%s` before %s, defined in %s:%d.' . PHP_EOL; $message .= 'Expected one of: %s'; $message = sprintf( $message, $token->getValue(), $pointcutExpression, (isset($reflection->class) ? $reflection->class . '->' : '') . $reflection->name, method_exists($reflection, 'getFileName') ? $reflection->getFileName() : $reflection->getDeclaringClass()->getFileName(), method_exists($reflection, 'getStartLine') ? $reflection->getStartLine() : 0, implode(', ', $e->getExpected()) ); throw new UnexpectedValueException($message, 0, $e); } return $pointcut; } } src/Core/AdviceMatcher.php000064400000017340144760133060011444 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Go\Aop; use Go\Aop\Support\NamespacedReflectionFunction; use Go\ParserReflection\ReflectionFileNamespace; use ReflectionClass; use ReflectionMethod; use ReflectionProperty; use function count; /** * Advice matcher returns the list of advices for the specific point of code */ class AdviceMatcher { /** * Flag to enable/disable support of global function interception * * @var bool */ private $isInterceptFunctions; /** * Constructor * * @param bool $isInterceptFunctions Optional flag to enable function interception */ public function __construct(bool $isInterceptFunctions = false) { $this->isInterceptFunctions = $isInterceptFunctions; } /** * Returns list of function advices for namespace * * @param Aop\Advisor[] $advisors List of advisor to match * * @return Aop\Advice[] List of advices for class */ public function getAdvicesForFunctions(ReflectionFileNamespace $namespace, array $advisors): array { if (!$this->isInterceptFunctions) { return []; } $advices = []; foreach ($advisors as $advisorId => $advisor) { if ($advisor instanceof Aop\PointcutAdvisor) { $pointcut = $advisor->getPointcut(); $isFunctionAdvisor = $pointcut->getKind() & Aop\PointFilter::KIND_FUNCTION; if ($isFunctionAdvisor && $pointcut->getClassFilter()->matches($namespace)) { $advices[] = $this->getFunctionAdvicesFromAdvisor($namespace, $advisor, $advisorId, $pointcut); } } } if (count($advices) > 0) { $advices = array_merge_recursive(...$advices); } return $advices; } /** * Return list of advices for class * * @param array|Aop\Advisor[] $advisors List of advisor to match * * @return Aop\Advice[] List of advices for class */ public function getAdvicesForClass(ReflectionClass $class, array $advisors): array { $classAdvices = []; $parentClass = $class->getParentClass(); $originalClass = $class; if ($parentClass && strpos($parentClass->name, AspectContainer::AOP_PROXIED_SUFFIX) !== false) { $originalClass = $parentClass; } foreach ($advisors as $advisorId => $advisor) { if ($advisor instanceof Aop\PointcutAdvisor) { $pointcut = $advisor->getPointcut(); if ($pointcut->getClassFilter()->matches($class)) { $classAdvices[] = $this->getAdvicesFromAdvisor($originalClass, $advisor, $advisorId, $pointcut); } } if ($advisor instanceof Aop\IntroductionAdvisor) { if ($advisor->getClassFilter()->matches($class)) { $classAdvices[] = $this->getIntroductionFromAdvisor($originalClass, $advisor, $advisorId); } } } if (count($classAdvices) > 0) { $classAdvices = array_merge_recursive(...$classAdvices); } return $classAdvices; } /** * Returns list of advices from advisor and point filter */ private function getAdvicesFromAdvisor( ReflectionClass $class, Aop\PointcutAdvisor $advisor, string $advisorId, Aop\PointFilter $filter ): array { $classAdvices = []; $filterKind = $filter->getKind(); // Check class only for class filters if ($filterKind & Aop\PointFilter::KIND_CLASS) { if ($filter->matches($class)) { // Dynamic initialization if ($filterKind & Aop\PointFilter::KIND_INIT) { $classAdvices[AspectContainer::INIT_PREFIX]['root'][$advisorId] = $advisor->getAdvice(); } // Static initalization if ($filterKind & Aop\PointFilter::KIND_STATIC_INIT) { $classAdvices[AspectContainer::STATIC_INIT_PREFIX]['root'][$advisorId] = $advisor->getAdvice(); } } } // Check methods in class only for method filters if ($filterKind & Aop\PointFilter::KIND_METHOD) { $mask = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED; foreach ($class->getMethods($mask) as $method) { // abstract and parent final methods could not be woven $isParentFinalMethod = ($method->getDeclaringClass()->name !== $class->name) && $method->isFinal(); if ($isParentFinalMethod || $method->isAbstract()) { continue; } if ($filter->matches($method, $class)) { $prefix = $method->isStatic() ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX; $classAdvices[$prefix][$method->name][$advisorId] = $advisor->getAdvice(); } } } // Check properties in class only for property filters if ($filterKind & Aop\PointFilter::KIND_PROPERTY) { $mask = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; foreach ($class->getProperties($mask) as $property) { if ($filter->matches($property, $class) && !$property->isStatic()) { $classAdvices[AspectContainer::PROPERTY_PREFIX][$property->name][$advisorId] = $advisor->getAdvice(); } } } return $classAdvices; } /** * Returns list of introduction advices from advisor */ private function getIntroductionFromAdvisor( ReflectionClass $class, Aop\IntroductionAdvisor $advisor, string $advisorId ): array { $classAdvices = []; // Do not make introduction for traits if ($class->isTrait()) { return $classAdvices; } /** @var Aop\IntroductionInfo $introduction */ $introduction = $advisor->getAdvice(); $introducedTrait = $introduction->getTrait(); if (!empty($introducedTrait)) { $introducedTrait = '\\' . ltrim($introducedTrait, '\\'); $classAdvices[AspectContainer::INTRODUCTION_TRAIT_PREFIX][$advisorId] = $introducedTrait; } $introducedInterface = $introduction->getInterface(); if (!empty($introducedInterface)) { $introducedInterface = '\\' . ltrim($introducedInterface, '\\'); $classAdvices[AspectContainer::INTRODUCTION_INTERFACE_PREFIX][$advisorId] = $introducedInterface; } return $classAdvices; } /** * Returns list of function advices for specific namespace */ private function getFunctionAdvicesFromAdvisor( ReflectionFileNamespace $namespace, Aop\PointcutAdvisor $advisor, string $advisorId, Aop\PointFilter $pointcut ): array { $functions = []; $advices = []; $listOfGlobalFunctions = get_defined_functions(); foreach ($listOfGlobalFunctions['internal'] as $functionName) { $functions[$functionName] = new NamespacedReflectionFunction($functionName, $namespace->getName()); } foreach ($functions as $functionName => $function) { if ($pointcut->matches($function, $namespace)) { $advices[AspectContainer::FUNCTION_PREFIX][$functionName][$advisorId] = $advisor->getAdvice(); } } return $advices; } } src/Core/AspectContainer.php000064400000006307144760133060012030 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Pointcut; /** * Aspect container interface */ interface AspectContainer { /** * Prefix for function interceptor */ public const FUNCTION_PREFIX = 'func'; /** * Prefix for properties interceptor */ public const PROPERTY_PREFIX = 'prop'; /** * Prefix for method interceptor */ public const METHOD_PREFIX = 'method'; /** * Prefix for static method interceptor */ public const STATIC_METHOD_PREFIX = 'static'; /** * Trait introduction prefix */ public const INTRODUCTION_TRAIT_PREFIX = 'trait'; /** * Interface introduction prefix */ public const INTRODUCTION_INTERFACE_PREFIX = 'interface'; /** * Initialization prefix, is used for initialization pointcuts */ public const INIT_PREFIX = 'init'; /** * Initialization prefix, is used for initialization pointcuts */ public const STATIC_INIT_PREFIX = 'staticinit'; /** * Suffix, that will be added to all proxied class names */ public const AOP_PROXIED_SUFFIX = '__AopProxied'; /** * Return a service or value from the container * * @return mixed * @throws \OutOfBoundsException if service was not found */ public function get(string $id); /** * Return list of service tagged with marker */ public function getByTag(string $tag): array; /** * Returns a pointcut by identifier */ public function getPointcut(string $id): Pointcut; /** * Checks if item with specified id is present in the container */ public function has(string $id): bool; /** * Store the pointcut in the container */ public function registerPointcut(Pointcut $pointcut, string $id): void; /** * Returns an advisor by identifier */ public function getAdvisor(string $id): Advisor; /** * Store the advisor in the container */ public function registerAdvisor(Advisor $advisor, string $id): void; /** * Register an aspect in the container */ public function registerAspect(Aspect $aspect): void; /** * Returns an aspect by id or class name */ public function getAspect(string $aspectName): Aspect; /** * Add an AOP resource to the container * Resources is used to check the freshness of AOP cache * * @param string $resource Path to the resource */ public function addResource(string $resource); /** * Returns the list of AOP resources */ public function getResources(): array; /** * Checks the freshness of AOP cache * * @return bool Whether or not concrete file is fresh */ public function isFresh(int $timestamp): bool; /** * Set a service into the container * * @param mixed $value Value to store */ public function set(string $id, $value, array $tags = []): void; } src/Core/AspectKernel.php000064400000020166144760133060011325 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Go\Aop\Features; use Go\Instrument\ClassLoading\AopComposerLoader; use Go\Instrument\ClassLoading\SourceTransformingLoader; use Go\Instrument\PathResolver; use Go\Instrument\Transformer\CachingTransformer; use Go\Instrument\Transformer\ConstructorExecutionTransformer; use Go\Instrument\Transformer\FilterInjectorTransformer; use Go\Instrument\Transformer\MagicConstantTransformer; use Go\Instrument\Transformer\SelfValueTransformer; use Go\Instrument\Transformer\SourceTransformer; use Go\Instrument\Transformer\WeavingTransformer; use ReflectionObject; use function define; /** * Abstract aspect kernel is used to prepare an application to work with aspects. */ abstract class AspectKernel { /** * Version of kernel */ public const VERSION = '3.0.0'; /** * Kernel options */ protected $options = [ 'features' => 0 ]; /** * Single instance of kernel * * @var static */ protected static $instance; /** * Default class name for container, can be redefined in children */ protected static $containerClass = GoAspectContainer::class; /** * Flag to determine if kernel was already initialized or not */ protected $wasInitialized = false; /** * Aspect container instance * * @var AspectContainer */ protected $container; /** * Protected constructor is used to prevent direct creation, but allows customization if needed */ protected function __construct() {} /** * Returns the single instance of kernel */ public static function getInstance(): self { if (!self::$instance) { self::$instance = new static(); } return self::$instance; } /** * Init the kernel and make adjustments * * @param array $options Associative array of options for kernel */ public function init(array $options = []) { if ($this->wasInitialized) { return; } $this->options = $this->normalizeOptions($options); define('AOP_ROOT_DIR', $this->options['appDir']); define('AOP_CACHE_DIR', $this->options['cacheDir']); /** @var AspectContainer $container */ $container = $this->container = new $this->options['containerClass']; $container->set('kernel', $this); $container->set('kernel.interceptFunctions', $this->hasFeature(Features::INTERCEPT_FUNCTIONS)); $container->set('kernel.options', $this->options); SourceTransformingLoader::register(); foreach ($this->registerTransformers() as $sourceTransformer) { SourceTransformingLoader::addTransformer($sourceTransformer); } // Register kernel resources in the container for debug mode if ($this->options['debug']) { $this->addKernelResourcesToContainer($container); } AopComposerLoader::init($this->options, $container); // Register all AOP configuration in the container $this->configureAop($container); $this->wasInitialized = true; } /** * Returns an aspect container */ public function getContainer(): AspectContainer { return $this->container; } /** * Checks if kernel configuration has enabled specific feature * * @see \Go\Aop\Features enumeration class for features */ public function hasFeature(int $featureToCheck): bool { return ($this->options['features'] & $featureToCheck) !== 0; } /** * Returns list of kernel options */ public function getOptions(): array { return $this->options; } /** * Returns default options for kernel. Available options: * * debug - boolean Determines whether or not kernel is in debug mode * appDir - string Path to the application root directory. * cacheDir - string Path to the cache directory where compiled classes will be stored * cacheFileMode - integer Binary mask of permission bits that is set to cache files * annotationCache - Doctrine\Common\Cache\Cache. If not provided, Doctrine\Common\Cache\PhpFileCache is used. * features - integer Binary mask of features * includePaths - array Whitelist of directories where aspects should be applied. Empty for everywhere. * excludePaths - array Blacklist of directories or files where aspects shouldn't be applied. */ protected function getDefaultOptions(): array { return [ 'debug' => false, 'appDir' => __DIR__ . '/../../../../../', 'cacheDir' => null, 'cacheFileMode' => 0770 & ~umask(), // Respect user umask() policy 'features' => 0, 'annotationCache' => null, 'includePaths' => [], 'excludePaths' => [], 'containerClass' => static::$containerClass, ]; } /** * Normalizes options for the kernel * * @param array $options List of options */ protected function normalizeOptions(array $options): array { $options = array_replace($this->getDefaultOptions(), $options); $options['cacheDir'] = PathResolver::realpath($options['cacheDir']); if (!$options['cacheDir']) { throw new \RuntimeException('You need to provide valid cache directory for Go! AOP framework.'); } $options['excludePaths'][] = $options['cacheDir']; $options['excludePaths'][] = __DIR__ . '/../'; $options['appDir'] = PathResolver::realpath($options['appDir']); $options['cacheFileMode'] = (int) $options['cacheFileMode']; $options['includePaths'] = PathResolver::realpath($options['includePaths']); $options['excludePaths'] = PathResolver::realpath($options['excludePaths']); return $options; } /** * Configures an AspectContainer with advisors, aspects and pointcuts */ abstract protected function configureAop(AspectContainer $container); /** * Returns list of source transformers, that will be applied to the PHP source * * @return SourceTransformer[] * @internal This method is internal and should not be used outside this project */ protected function registerTransformers(): array { $cacheManager = $this->getContainer()->get('aspect.cache.path.manager'); $filterInjector = new FilterInjectorTransformer($this, SourceTransformingLoader::getId(), $cacheManager); $magicTransformer = new MagicConstantTransformer($this); $sourceTransformers = function () use ($filterInjector, $magicTransformer, $cacheManager) { $transformers = []; if ($this->hasFeature(Features::INTERCEPT_INITIALIZATIONS)) { $transformers[] = new ConstructorExecutionTransformer(); } if ($this->hasFeature(Features::INTERCEPT_INCLUDES)) { $transformers[] = $filterInjector; } $transformers[] = new SelfValueTransformer($this); $transformers[] = new WeavingTransformer( $this, $this->container->get('aspect.advice_matcher'), $cacheManager, $this->container->get('aspect.cached.loader') ); $transformers[] = $magicTransformer; return $transformers; }; return [ new CachingTransformer($this, $sourceTransformers, $cacheManager) ]; } /** * Add resources of kernel to the container */ protected function addKernelResourcesToContainer(AspectContainer $container): void { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); $refClass = new ReflectionObject($this); $container->addResource($trace[1]['file']); $container->addResource($refClass->getFileName()); } } src/Core/AspectLoader.php000064400000014334144760133060011313 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Doctrine\Common\Annotations\Reader; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Pointcut; use InvalidArgumentException; use ReflectionClass; use ReflectionMethod; use ReflectionProperty; use Reflector; use function get_class; /** * Loader of aspects into the container */ class AspectLoader { /** * Aspect container instance */ protected $container; /** * List of aspect loaders * * @var AspectLoaderExtension[][] */ protected $loaders = []; /** * Annotation reader for aspects */ protected $annotationReader; /** * List of aspect class names that were loaded * * @var string[] */ protected $loadedAspects = []; /** * Loader constructor */ public function __construct(AspectContainer $container, Reader $reader) { $this->container = $container; $this->annotationReader = $reader; } /** * Register an aspect loader extension * * This method allows to extend the logic of aspect loading by registering an extension for loader. */ public function registerLoaderExtension(AspectLoaderExtension $loader): void { $targets = $loader->getTargets(); foreach ($targets as $target) { $this->loaders[$target][] = $loader; } } /** * Loads an aspect with the help of aspect loaders, but don't register it in the container * * @see loadAndRegister() method for registration * * @return Pointcut[]|Advisor[] */ public function load(Aspect $aspect): array { $loadedItems = []; $refAspect = new \ReflectionClass($aspect); if (!empty($this->loaders[AspectLoaderExtension::TARGET_CLASS])) { $loadedItems += $this->loadFrom($aspect, $refAspect, $this->loaders[AspectLoaderExtension::TARGET_CLASS]); } if (!empty($this->loaders[AspectLoaderExtension::TARGET_METHOD])) { $refMethods = $refAspect->getMethods(); foreach ($refMethods as $refMethod) { $loadedItems += $this->loadFrom($aspect, $refMethod, $this->loaders[AspectLoaderExtension::TARGET_METHOD]); } } if (!empty($this->loaders[AspectLoaderExtension::TARGET_PROPERTY])) { $refProperties = $refAspect->getProperties(); foreach ($refProperties as $refProperty) { $loadedItems += $this->loadFrom($aspect, $refProperty, $this->loaders[AspectLoaderExtension::TARGET_PROPERTY]); } } return $loadedItems; } /** * Loads and register all items of aspect in the container */ public function loadAndRegister(Aspect $aspect): void { $loadedItems = $this->load($aspect); foreach ($loadedItems as $itemId => $item) { if ($item instanceof Pointcut) { $this->container->registerPointcut($item, $itemId); } if ($item instanceof Advisor) { $this->container->registerAdvisor($item, $itemId); } } $aspectClass = get_class($aspect); $this->loadedAspects[$aspectClass] = $aspectClass; } /** * Returns list of unloaded aspects in the container * * @return Aspect[] */ public function getUnloadedAspects(): array { $unloadedAspects = []; foreach ($this->container->getByTag('aspect') as $aspect) { if (!isset($this->loadedAspects[get_class($aspect)])) { $unloadedAspects[] = $aspect; } } return $unloadedAspects; } /** * Load definitions from specific aspect part into the aspect container * * @param Aspect $aspect Aspect instance * @param Reflector $reflector Reflection instance * @param array|AspectLoaderExtension[] $loaders List of loaders that can produce advisors from aspect class * * @throws \InvalidArgumentException If kind of loader isn't supported * * @return array|Pointcut[]|Advisor[] */ protected function loadFrom(Aspect $aspect, Reflector $reflector, array $loaders): array { $loadedItems = []; foreach ($loaders as $loader) { $loaderKind = $loader->getKind(); switch ($loaderKind) { case AspectLoaderExtension::KIND_REFLECTION: if ($loader->supports($aspect, $reflector)) { $loadedItems += $loader->load($aspect, $reflector); } break; case AspectLoaderExtension::KIND_ANNOTATION: $annotations = $this->getAnnotations($reflector); foreach ($annotations as $annotation) { if ($loader->supports($aspect, $reflector, $annotation)) { $loadedItems += $loader->load($aspect, $reflector, $annotation); } } break; default: throw new InvalidArgumentException("Unsupported loader kind {$loaderKind}"); } } return $loadedItems; } /** * Return list of annotations for reflection point * * @return array list of annotations * @throws \InvalidArgumentException if $reflector is unsupported */ protected function getAnnotations(Reflector $reflector): array { switch (true) { case ($reflector instanceof ReflectionClass): return $this->annotationReader->getClassAnnotations($reflector); case ($reflector instanceof ReflectionMethod): return $this->annotationReader->getMethodAnnotations($reflector); case ($reflector instanceof ReflectionProperty): return $this->annotationReader->getPropertyAnnotations($reflector); default: throw new InvalidArgumentException('Unsupported reflection point ' . get_class($reflector)); } } } src/Core/AspectLoaderExtension.php000064400000004443144760133060013210 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Pointcut; use Reflector; /** * Extension interface that defines an API for aspect loaders */ interface AspectLoaderExtension { /** * Loader works with class-level definition of aspect */ public const TARGET_CLASS = 'class'; /** * Loader works with method definitions of aspect */ public const TARGET_METHOD = 'method'; /** * Loader works with property definitions of aspect */ public const TARGET_PROPERTY = 'property'; /** * Loader works only with reflections of aspect class, method or property */ public const KIND_REFLECTION = 'reflection'; /** * Loader works with each annotation defined for aspect class, method or property. */ public const KIND_ANNOTATION = 'annotation'; /** * Return kind of loader, can be one of KIND_REFLECTION or KIND_ANNOTATION * * For loader that works with annotations additional metaInformation will be passed */ public function getKind(): string; /** * Returns one or more target for loader, see TARGET_XXX constants */ public function getTargets(): array; /** * Checks if loader is able to handle specific point of aspect * * @param Aspect $aspect Instance of aspect * @param mixed|\ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflection Reflection of point * @param mixed|null $metaInformation Additional meta-information, e.g. annotation for method */ public function supports(Aspect $aspect, $reflection, $metaInformation = null): bool; /** * Loads definition from specific point of aspect into the container * * @param Aspect $aspect Instance of aspect * @param Reflector $reflection Reflection of point * @param mixed|null $metaInformation Additional meta-information, e.g. annotation for method * * @return array|Pointcut[]|Advisor[] */ public function load(Aspect $aspect, Reflector $reflection, $metaInformation = null): array; } src/Core/CachedAspectLoader.php000064400000006324144760133060012403 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Pointcut; use ReflectionClass; /** * Cached loader is responsible for faster initialization of pointcuts/advisors for concrete aspect * * @property AspectLoader $loader */ class CachedAspectLoader extends AspectLoader { /** * Path to the cache directory * * @var null|string */ protected $cacheDir; /** * File mode for the cache files * * @var integer */ protected $cacheFileMode; /** * Identifier of original loader */ protected $loaderId; /** * Cached loader constructor * * @param array $options List of kernel options */ public function __construct(AspectContainer $container, string $loaderId, array $options = []) { $this->cacheDir = $options['cacheDir'] ?? null; $this->cacheFileMode = $options['cacheFileMode']; $this->loaderId = $loaderId; $this->container = $container; } /** * {@inheritdoc} */ public function load(Aspect $aspect): array { $refAspect = new ReflectionClass($aspect); $fileName = $this->cacheDir . '/_aspect/' . sha1($refAspect->getName()); // If cache is present and actual, then use it if (file_exists($fileName) && filemtime($fileName) >= filemtime($refAspect->getFileName())) { $loadedItems = $this->loadFromCache($fileName); } else { $loadedItems = $this->loader->load($aspect); $this->saveToCache($loadedItems, $fileName); } return $loadedItems; } /** * {@inheritdoc} */ public function registerLoaderExtension(AspectLoaderExtension $loader): void { $this->loader->registerLoaderExtension($loader); } /** * {@inheritdoc} */ public function __get($name) { if ($name === 'loader') { $this->loader = $this->container->get($this->loaderId); return $this->loader; } throw new \RuntimeException('Not implemented'); } /** * Loads pointcuts and advisors from the file * * @return Pointcut[]|Advisor[] */ protected function loadFromCache(string $fileName): array { $content = file_get_contents($fileName); $loadedItems = unserialize($content); return $loadedItems; } /** * Save pointcuts and advisors to the file * * @param Pointcut[]|Advisor[] $items List of items to store */ protected function saveToCache(array $items, string $fileName): void { $content = serialize($items); $directoryName = dirname($fileName); if (!is_dir($directoryName)) { mkdir($directoryName, $this->cacheFileMode, true); } file_put_contents($fileName, $content, LOCK_EX); // For cache files we don't want executable bits by default chmod($fileName, $this->cacheFileMode & (~0111)); } } src/Core/Container.php000064400000004407144760133060010667 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Closure; use OutOfBoundsException; /** * DI-container */ abstract class Container implements AspectContainer { /** * List of services in the container */ protected $values = []; /** * Store identifiers os services by tags */ protected $tags = []; /** * Set a service into the container * * @param mixed $value Value to store */ public function set(string $id, $value, array $tags = []): void { $this->values[$id] = $value; foreach ($tags as $tag) { $this->tags[$tag][] = $id; } } /** * Set a shared value in the container */ public function share(string $id, Closure $value, array $tags = []): void { $value = function ($container) use ($value) { static $sharedValue; if ($sharedValue === null) { $sharedValue = $value($container); } return $sharedValue; }; $this->set($id, $value, $tags); } /** * Return a service or value from the container * * @return mixed * @throws OutOfBoundsException if service was not found */ public function get(string $id) { if (!isset($this->values[$id])) { throw new OutOfBoundsException("Value {$id} is not defined in the container"); } if ($this->values[$id] instanceof Closure) { return $this->values[$id]($this); } return $this->values[$id]; } /** * Checks if item with specified id is present in the container */ public function has(string $id): bool { return isset($this->values[$id]); } /** * Return list of service tagged with marker */ public function getByTag(string $tag): array { $result = []; if (isset($this->tags[$tag])) { foreach ($this->tags[$tag] as $id) { $result[$id] = $this->get($id); } } return $result; } } src/Core/GeneralAspectLoaderExtension.php000064400000010624144760133060014504 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Closure; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Framework; use Go\Aop\Intercept\Interceptor; use Go\Aop\Pointcut; use Go\Aop\Support\DefaultPointcutAdvisor; use Go\Lang\Annotation; use Go\Lang\Annotation\BaseInterceptor; use ReflectionMethod; use Reflector; use UnexpectedValueException; use function get_class; /** * General aspect loader add common support for general advices, declared as annotations */ class GeneralAspectLoaderExtension extends AbstractAspectLoaderExtension { /** * General aspect loader works with annotations from aspect */ public function getKind(): string { return self::KIND_ANNOTATION; } /** * General aspect loader works only with methods of aspect */ public function getTargets(): array { return [self::TARGET_METHOD]; } /** * Checks if loader is able to handle specific point of aspect * * @param Aspect $aspect Instance of aspect * @param mixed|\ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflection Reflection of point * @param mixed|null $metaInformation Additional meta-information, e.g. annotation for method * * @return boolean true if extension is able to create an advisor from reflection and metaInformation */ public function supports(Aspect $aspect, $reflection, $metaInformation = null): bool { return $metaInformation instanceof Annotation\Interceptor || $metaInformation instanceof Annotation\Pointcut; } /** * Loads definition from specific point of aspect into the container * * @param Aspect $aspect Instance of aspect * @param Reflector|ReflectionMethod $reflection Reflection of point * @param mixed|null $metaInformation Additional meta-information, e.g. annotation for method * * @return array|Pointcut[]|Advisor[] * * @throws UnexpectedValueException */ public function load(Aspect $aspect, Reflector $reflection, $metaInformation = null): array { $loadedItems = []; $pointcut = $this->parsePointcut($aspect, $reflection, $metaInformation->value); $methodId = get_class($aspect) . '->' . $reflection->name; $adviceCallback = $reflection->getClosure($aspect); switch (true) { // Register a pointcut by its name case ($metaInformation instanceof Annotation\Pointcut): $loadedItems[$methodId] = $pointcut; break; case ($pointcut instanceof Pointcut): $advice = $this->getInterceptor($metaInformation, $adviceCallback); $loadedItems[$methodId] = new DefaultPointcutAdvisor($pointcut, $advice); break; default: throw new UnexpectedValueException('Unsupported pointcut class: ' . get_class($pointcut)); } return $loadedItems; } /** * Returns an interceptor instance by meta-type annotation and closure * * @throws UnexpectedValueException For unsupported annotations */ protected function getInterceptor(BaseInterceptor $metaInformation, Closure $adviceCallback): Interceptor { $adviceOrder = $metaInformation->order; $pointcutExpression = $metaInformation->value; switch (true) { case ($metaInformation instanceof Annotation\Before): return new Framework\BeforeInterceptor($adviceCallback, $adviceOrder, $pointcutExpression); case ($metaInformation instanceof Annotation\After): return new Framework\AfterInterceptor($adviceCallback, $adviceOrder, $pointcutExpression); case ($metaInformation instanceof Annotation\Around): return new Framework\AroundInterceptor($adviceCallback, $adviceOrder, $pointcutExpression); case ($metaInformation instanceof Annotation\AfterThrowing): return new Framework\AfterThrowingInterceptor($adviceCallback, $adviceOrder, $pointcutExpression); default: throw new UnexpectedValueException('Unsupported method meta class: ' . get_class($metaInformation)); } } } src/Core/GoAspectContainer.php000064400000014507144760133060012317 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Cache as DoctrineCache; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Pointcut; use Go\Aop\Pointcut\PointcutGrammar; use Go\Aop\Pointcut\PointcutLexer; use Go\Aop\Pointcut\PointcutParser; use Go\Instrument\ClassLoading\CachePathManager; use ReflectionClass; /** * Aspect container contains list of all pointcuts and advisors */ class GoAspectContainer extends Container { /** * List of resources for application * * @var string[] */ protected $resources = []; /** * Cached timestamp for resources * * @var integer */ protected $maxTimestamp = 0; /** * Constructor for container */ public function __construct() { // Register all services in the container $this->share('aspect.loader', function (Container $container) { $aspectLoader = new AspectLoader( $container, $container->get('aspect.annotation.reader') ); $lexer = $container->get('aspect.pointcut.lexer'); $parser = $container->get('aspect.pointcut.parser'); // Register general aspect loader extension $aspectLoader->registerLoaderExtension(new GeneralAspectLoaderExtension($lexer, $parser)); $aspectLoader->registerLoaderExtension(new IntroductionAspectExtension($lexer, $parser)); return $aspectLoader; }); $this->share('aspect.cached.loader', function (Container $container) { $options = $container->get('kernel.options'); if (!empty($options['cacheDir'])) { $loader = new CachedAspectLoader( $container, 'aspect.loader', $container->get('kernel.options') ); } else { $loader = $container->get('aspect.loader'); } return $loader; }); $this->share('aspect.advisor.accessor', function (Container $container) { return new LazyAdvisorAccessor( $container, $container->get('aspect.cached.loader') ); }); $this->share('aspect.advice_matcher', function (Container $container) { return new AdviceMatcher( $container->get('kernel.interceptFunctions') ); }); $this->share('aspect.annotation.cache', function (Container $container) { $options = $container->get('kernel.options'); if (!empty($options['annotationCache'])) { return $options['annotationCache']; } if (!empty($options['cacheDir'])) { return new DoctrineCache\FilesystemCache( $options['cacheDir'] . DIRECTORY_SEPARATOR . '_annotations' . DIRECTORY_SEPARATOR, '.annotations.cache', 0777 & (~$options['cacheFileMode']) ); } return new DoctrineCache\ArrayCache(); }); $this->share('aspect.annotation.reader', function (Container $container) { $options = $container->get('kernel.options'); return new CachedReader( new AnnotationReader(), $container->get('aspect.annotation.cache'), $options['debug'] ?? false ); }); $this->share('aspect.cache.path.manager', function (Container $container) { return new CachePathManager($container->get('kernel')); }); // Pointcut services $this->share('aspect.pointcut.lexer', function () { return new PointcutLexer(); }); $this->share('aspect.pointcut.parser', function (Container $container) { return new PointcutParser( new PointcutGrammar( $container, $container->get('aspect.annotation.reader') ) ); }); } /** * Returns a pointcut by identifier */ public function getPointcut(string $id): Pointcut { return $this->get("pointcut.{$id}"); } /** * Store the pointcut in the container */ public function registerPointcut(Pointcut $pointcut, string $id): void { $this->set("pointcut.{$id}", $pointcut, ['pointcut']); } /** * Returns an advisor by identifier */ public function getAdvisor(string $id): Advisor { return $this->get("advisor.{$id}"); } /** * Store the advisor in the container */ public function registerAdvisor(Advisor $advisor, string $id): void { $this->set("advisor.{$id}", $advisor, ['advisor']); } /** * Returns an aspect by id or class name */ public function getAspect(string $aspectName): Aspect { return $this->get("aspect.{$aspectName}"); } /** * Register an aspect in the container */ public function registerAspect(Aspect $aspect): void { $refAspect = new ReflectionClass($aspect); $this->set("aspect.{$refAspect->name}", $aspect, ['aspect']); $this->addResource($refAspect->getFileName()); } /** * Add an AOP resource to the container * Resources is used to check the freshness of AOP cache * * @param string $resource Path to the resource */ public function addResource(string $resource) { $this->resources[] = $resource; $this->maxTimestamp = 0; } /** * Returns list of AOP resources */ public function getResources(): array { return $this->resources; } /** * Checks the freshness of AOP cache * * @return bool Whether or not concrete file is fresh */ public function isFresh(int $timestamp): bool { if (!$this->maxTimestamp && !empty($this->resources)) { $this->maxTimestamp = max(array_map('filemtime', $this->resources)); } return $this->maxTimestamp <= $timestamp; } } src/Core/IntroductionAspectExtension.php000064400000006623144760133060014465 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Framework; use Go\Aop\Pointcut; use Go\Aop\Support; use Go\Lang\Annotation; use ReflectionProperty; use Reflector; use UnexpectedValueException; /** * Introduction aspect extension */ class IntroductionAspectExtension extends AbstractAspectLoaderExtension { /** * Introduction aspect loader works with annotations from aspect */ public function getKind(): string { return self::KIND_ANNOTATION; } /** * Introduction aspect loader works only with properties of aspect */ public function getTargets(): array { return [self::TARGET_PROPERTY]; } /** * Checks if loader is able to handle specific point of aspect * * @param Aspect $aspect Instance of aspect * @param mixed|\ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflection Reflection of point * @param mixed|null $metaInformation Additional meta-information, e.g. annotation for method * * @return boolean true if extension is able to create an advisor from reflection and metaInformation */ public function supports(Aspect $aspect, $reflection, $metaInformation = null): bool { return ($metaInformation instanceof Annotation\DeclareParents) || ($metaInformation instanceof Annotation\DeclareError); } /** * Loads definition from specific point of aspect into the container * * @param Aspect $aspect Instance of aspect * @param Reflector|ReflectionProperty $reflection Reflection of point * @param mixed|null $metaInformation Additional meta-information * * @throws UnexpectedValueException * * @return Pointcut[]|Advisor[] */ public function load(Aspect $aspect, $reflection, $metaInformation = null): array { $loadedItems = []; $pointcut = $this->parsePointcut($aspect, $reflection, $metaInformation->value); $propertyId = $reflection->class . '->' . $reflection->name; switch (true) { case ($metaInformation instanceof Annotation\DeclareParents): $implement = $metaInformation->defaultImpl; $interface = $metaInformation->interface; $advice = new Framework\TraitIntroductionInfo($implement, $interface); $advisor = new Support\DeclareParentsAdvisor($pointcut->getClassFilter(), $advice); $loadedItems[$propertyId] = $advisor; break; case (($metaInformation instanceof Annotation\DeclareError) && ($pointcut instanceof Pointcut)): $reflection->setAccessible(true); $message = $reflection->getValue($aspect); $level = $metaInformation->level; $advice = new Framework\DeclareErrorInterceptor($message, $level, $metaInformation->value); $loadedItems[$propertyId] = new Support\DefaultPointcutAdvisor($pointcut, $advice); break; default: throw new UnexpectedValueException('Unsupported pointcut class: ' . get_class($pointcut)); } return $loadedItems; } } src/Core/LazyAdvisorAccessor.php000064400000003164144760133060012676 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Core; use Go\Aop\Advice; use Go\Aop\Advisor; use InvalidArgumentException; /** * Provides an interface for loading of advisors from the container */ class LazyAdvisorAccessor { /** * Instance of aspect container */ protected $container; /** * Aspect loader instance */ protected $loader; /** * Accessor constructor */ public function __construct(AspectContainer $container, AspectLoader $loader) { $this->container = $container; $this->loader = $loader; } /** * Magic advice accessor * * @throws InvalidArgumentException if referenced value is not an advisor */ public function __get(string $name): Advice { if ($this->container->has($name)) { $advisor = $this->container->get($name); } else { list(, $advisorName) = explode('.', $name); list($aspect) = explode('->', $advisorName); $aspectInstance = $this->container->getAspect($aspect); $this->loader->loadAndRegister($aspectInstance); $advisor = $this->container->get($name); } if (!$advisor instanceof Advisor) { throw new InvalidArgumentException("Reference {$name} is not an advisor"); } $this->$name = $advisor->getAdvice(); return $this->$name; } } src/Instrument/ClassLoading/AopComposerLoader.php000064400000011502144760133060016140 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\ClassLoading; use Go\Core\AspectContainer; use Go\Instrument\FileSystem\Enumerator; use Go\Instrument\PathResolver; use Go\Instrument\Transformer\FilterInjectorTransformer; use Composer\Autoload\ClassLoader; use Doctrine\Common\Annotations\AnnotationRegistry; /** * AopComposerLoader class is responsible to use a weaver for classes instead of original one */ class AopComposerLoader { /** * Instance of original autoloader */ protected $original; /** * AOP kernel options */ protected $options = []; /** * File enumerator */ protected $fileEnumerator; /** * Cache state * * @var array */ private $cacheState; /** * Was initialization successful or not */ private static $wasInitialized = false; /** * Constructs an wrapper for the composer loader * * @param array $options Configuration options */ public function __construct(ClassLoader $original, AspectContainer $container, array $options = []) { $this->options = $options; $this->original = $original; $prefixes = $original->getPrefixes(); $excludePaths = $options['excludePaths']; if (!empty($prefixes)) { // Let's exclude core dependencies from that list if (isset($prefixes['Dissect'])) { $excludePaths[] = $prefixes['Dissect'][0]; } if (isset($prefixes['Doctrine\\Common\\Annotations\\'])) { $excludePaths[] = substr($prefixes['Doctrine\\Common\\Annotations\\'][0], 0, -16); } } $fileEnumerator = new Enumerator($options['appDir'], $options['includePaths'], $excludePaths); $this->fileEnumerator = $fileEnumerator; $this->cacheState = $container->get('aspect.cache.path.manager')->queryCacheState(); } /** * Initialize aspect autoloader and returns status whether initialization was successful or not * * Replaces original composer autoloader with wrapper * * @param array $options Aspect kernel options */ public static function init(array $options, AspectContainer $container): bool { $loaders = spl_autoload_functions(); foreach ($loaders as &$loader) { $loaderToUnregister = $loader; if (is_array($loader) && ($loader[0] instanceof ClassLoader)) { $originalLoader = $loader[0]; // Configure library loader for doctrine annotation loader AnnotationRegistry::registerLoader(function($class) use ($originalLoader) { $originalLoader->loadClass($class); return class_exists($class, false); }); $loader[0] = new AopComposerLoader($loader[0], $container, $options); self::$wasInitialized = true; } spl_autoload_unregister($loaderToUnregister); } unset($loader); foreach ($loaders as $loader) { spl_autoload_register($loader); } return self::$wasInitialized; } /** * Autoload a class by it's name */ public function loadClass(string $class): void { $file = $this->findFile($class); if ($file !== false) { include $file; } } /** * Finds either the path to the file where the class is defined, * or gets the appropriate php://filter stream for the given class. * * @return string|false The path/resource if found, false otherwise. */ public function findFile(string $class) { static $isAllowedFilter = null, $isProduction = false; if (!$isAllowedFilter) { $isAllowedFilter = $this->fileEnumerator->getFilter(); $isProduction = !$this->options['debug']; } $file = $this->original->findFile($class); if ($file !== false) { $file = PathResolver::realpath($file)?:$file; $cacheState = $this->cacheState[$file] ?? null; if ($cacheState && $isProduction) { $file = $cacheState['cacheUri'] ?: $file; } elseif ($isAllowedFilter(new \SplFileInfo($file))) { // can be optimized here with $cacheState even for debug mode, but no needed right now $file = FilterInjectorTransformer::rewrite($file); } } return $file; } /** * Whether or not loader was initialized */ public static function wasInitialized(): bool { return self::$wasInitialized; } } src/Instrument/ClassLoading/CachePathManager.php000064400000013505144760133060015702 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\ClassLoading; use Go\Aop\Features; use Go\Core\AspectKernel; use InvalidArgumentException; use function function_exists; /** * Class that manages real-code to cached-code paths mapping. * Can be extended to get a more sophisticated real-to-cached code mapping */ class CachePathManager { /** * Name of the file with cache paths */ private const CACHE_FILE_NAME = '/_transformation.cache'; /** * @var array */ protected $options = []; /** * Aspect kernel instance */ protected $kernel; /** * @var string|null */ protected $cacheDir; /** * File mode * * @var integer */ protected $fileMode; /** * @var string|null */ protected $appDir; /** * Cached metadata for transformation state for the concrete file * * @var array */ protected $cacheState = []; /** * New metadata items, that was not present in $cacheState * * @var array */ protected $newCacheState = []; public function __construct(AspectKernel $kernel) { $this->kernel = $kernel; $this->options = $kernel->getOptions(); $this->appDir = $this->options['appDir']; $this->cacheDir = $this->options['cacheDir']; $this->fileMode = $this->options['cacheFileMode']; if ($this->cacheDir) { if (!is_dir($this->cacheDir)) { $cacheRootDir = dirname($this->cacheDir); if (!is_writable($cacheRootDir) || !is_dir($cacheRootDir)) { throw new InvalidArgumentException( "Can not create a directory {$this->cacheDir} for the cache. Parent directory {$cacheRootDir} is not writable or not exist."); } mkdir($this->cacheDir, $this->fileMode, true); } if (!$this->kernel->hasFeature(Features::PREBUILT_CACHE) && !is_writable($this->cacheDir)) { throw new InvalidArgumentException("Cache directory {$this->cacheDir} is not writable"); } if (file_exists($this->cacheDir . self::CACHE_FILE_NAME)) { $this->cacheState = include $this->cacheDir . self::CACHE_FILE_NAME; } } } /** * Returns current cache directory for aspects, can be bull */ public function getCacheDir(): ?string { return $this->cacheDir; } /** * Configures a new cache directory for aspects */ public function setCacheDir(string $cacheDir): void { $this->cacheDir = $cacheDir; } /** * Returns cache path for requested file name * * @return bool|string */ public function getCachePathForResource(string $resource) { if (!$this->cacheDir) { return false; } return str_replace($this->appDir, $this->cacheDir, $resource); } /** * Tries to return an information for queried resource * * @param string|null $resource Name of the file or null to get all information * * @return array|null Information or null if no record in the cache */ public function queryCacheState(string $resource = null): ?array { if ($resource === null) { return $this->cacheState; } if (isset($this->newCacheState[$resource])) { return $this->newCacheState[$resource]; } if (isset($this->cacheState[$resource])) { return $this->cacheState[$resource]; } return null; } /** * Put a record about some resource in the cache * * This data will be persisted during object destruction * * @param array $metadata Miscellaneous information about resource */ public function setCacheState(string $resource, array $metadata): void { $this->newCacheState[$resource] = $metadata; } /** * Automatic destructor saves all new changes into the cache * * This implementation is not thread-safe, so be care */ public function __destruct() { $this->flushCacheState(); } /** * Flushes the cache state into the file */ public function flushCacheState(bool $force = false): void { if ((!empty($this->newCacheState) && is_writable($this->cacheDir)) || $force) { $fullCacheMap = $this->newCacheState + $this->cacheState; $cachePath = substr(var_export($this->cacheDir, true), 1, -1); $rootPath = substr(var_export($this->appDir, true), 1, -1); $cacheData = ' 'AOP_CACHE_DIR . \'', '\'' . $rootPath => 'AOP_ROOT_DIR . \'' ]); $fullCacheFileName = $this->cacheDir . self::CACHE_FILE_NAME; file_put_contents($fullCacheFileName, $cacheData, LOCK_EX); // For cache files we don't want executable bits by default chmod($fullCacheFileName, $this->fileMode & (~0111)); if (function_exists('opcache_invalidate')) { opcache_invalidate($fullCacheFileName, true); } $this->cacheState = $this->newCacheState + $this->cacheState; $this->newCacheState = []; } } /** * Clear the cache state. */ public function clearCacheState(): void { $this->cacheState = []; $this->newCacheState = []; $this->flushCacheState(true); } } src/Instrument/ClassLoading/CacheWarmer.php000064400000006240144760133060014746 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\ClassLoading; use ErrorException; use Go\Core\AspectKernel; use Go\Instrument\FileSystem\Enumerator; use Go\Instrument\Transformer\FilterInjectorTransformer; use InvalidArgumentException; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Throwable; use function count; /** * Warms up the cache */ class CacheWarmer { /** * Instance of aspect kernel */ protected $aspectKernel; /** * Output instance */ protected $output; /** * CacheWarmer constructor. */ public function __construct(AspectKernel $aspectKernel, OutputInterface $output = null) { $this->aspectKernel = $aspectKernel; $this->output = $output ?? new NullOutput(); } /** * Warms up cache */ public function warmUp(): void { $options = $this->aspectKernel->getOptions(); if (empty($options['cacheDir'])) { throw new InvalidArgumentException('Cache warmer require the `cacheDir` options to be configured'); } $enumerator = new Enumerator($options['appDir'], $options['includePaths'], $options['excludePaths']); $iterator = $enumerator->enumerate(); $total = iterator_count($iterator); $this->output->writeln(sprintf('Total %s files to process.', $total)); $this->output->writeln(''); $iterator->rewind(); set_error_handler(function ($errno, $errstr, $errfile, $errline) { throw new ErrorException($errstr, $errno, 0, $errfile, $errline); }); $errors = []; $displayException = function (Throwable $exception, string $path) use (&$errors) { $this->output->writeln(sprintf('[ERR]: %s', $path)); $errors[$path] = $exception->getMessage(); }; foreach ($iterator as $file) { $path = $file->getRealPath(); try { // This will trigger creation of cache file_get_contents(FilterInjectorTransformer::PHP_FILTER_READ . SourceTransformingLoader::FILTER_IDENTIFIER . '/resource=' . $path ); $this->output->writeln(sprintf('[OK]: %s', $path)); } catch (Throwable $e) { $displayException($e, $path); } } restore_error_handler(); if ($this->output->isVerbose()) { foreach ($errors as $path => $error) { $this->output->writeln(sprintf('[ERR]: File "%s" is not processed correctly due to exception: "%s".', $path, $error)); } } $this->output->writeln(''); $this->output->writeln(sprintf('[DONE]: Total processed %s, %s errors.', $total, count($errors))); } } src/Instrument/ClassLoading/SourceTransformingLoader.php000064400000006544144760133060017555 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\ClassLoading; use php_user_filter as PhpStreamFilter; use Go\Instrument\Transformer\StreamMetaData; use Go\Instrument\Transformer\SourceTransformer; use RuntimeException; use function strlen; /** * Php class loader filter for processing php code * * @property resource $stream Stream instance of underlying resource */ class SourceTransformingLoader extends PhpStreamFilter { /** * Php filter definition */ public const PHP_FILTER_READ = 'php://filter/read='; /** * Default PHP filter name for registration */ public const FILTER_IDENTIFIER = 'go.source.transforming.loader'; /** * String buffer */ protected $data = ''; /** * List of transformers * * @var SourceTransformer[] */ protected static $transformers = []; /** * Identifier of filter * * @var string */ protected static $filterId; /** * Register current loader as stream filter in PHP * * @throws RuntimeException If registration was failed */ public static function register(string $filterId = self::FILTER_IDENTIFIER): void { if (!empty(self::$filterId)) { throw new RuntimeException('Stream filter already registered'); } $result = stream_filter_register($filterId, __CLASS__); if ($result === false) { throw new RuntimeException('Stream filter was not registered'); } self::$filterId = $filterId; } /** * Returns the name of registered filter * * @throws RuntimeException if filter was not registered */ public static function getId(): string { if (empty(self::$filterId)) { throw new RuntimeException('Stream filter was not registered'); } return self::$filterId; } /** * {@inheritdoc} */ public function filter($in, $out, &$consumed, $closing) { while ($bucket = stream_bucket_make_writeable($in)) { $this->data .= $bucket->data; } if ($closing || feof($this->stream)) { $consumed = strlen($this->data); // $this->stream contains pointer to the source $metadata = new StreamMetaData($this->stream, $this->data); self::transformCode($metadata); $bucket = stream_bucket_new($this->stream, $metadata->source); stream_bucket_append($out, $bucket); return PSFS_PASS_ON; } return PSFS_FEED_ME; } /** * Adds a SourceTransformer to be applied by this LoadTimeWeaver. */ public static function addTransformer(SourceTransformer $transformer): void { self::$transformers[] = $transformer; } /** * Transforms source code by passing it through all transformers */ public static function transformCode(StreamMetaData $metadata): void { foreach (self::$transformers as $transformer) { $result = $transformer->transform($metadata); if ($result === SourceTransformer::RESULT_ABORTED) { break; } } } } src/Instrument/FileSystem/Enumerator.php000064400000011530144760133060014425 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\FileSystem; use ArrayIterator; use Closure; use InvalidArgumentException; use Iterator; use LogicException; use SplFileInfo; use Symfony\Component\Finder\Finder; use UnexpectedValueException; /** * Enumerates files in the concrete directory, applying filtration logic */ class Enumerator { /** * Path to the root directory, where enumeration should start */ private $rootDirectory; /** * List of additional include paths, should be below rootDirectory */ private $includePaths; /** * List of additional exclude paths, should be below rootDirectory */ private $excludePaths; /** * Initializes an enumerator * * @param string $rootDirectory Path to the root directory * @param array $includePaths List of additional include paths * @param array $excludePaths List of additional exclude paths */ public function __construct(string $rootDirectory, array $includePaths = [], array $excludePaths = []) { $this->rootDirectory = $rootDirectory; $this->includePaths = $includePaths; $this->excludePaths = $excludePaths; } /** * Returns an enumerator for files * * @return Iterator|SplFileInfo[] * @throws UnexpectedValueException * @throws InvalidArgumentException * @throws LogicException */ public function enumerate(): Iterator { $finder = new Finder(); $finder->files() ->name('*.php') ->in($this->getInPaths()); foreach ($this->getExcludePaths() as $path) { $finder->notPath($path); } $iterator = $finder->getIterator(); // on Windows platform the default iterator is unable to rewind, not sure why if (strpos(PHP_OS, 'WIN') === 0) { $iterator = new ArrayIterator(iterator_to_array($iterator)); } return $iterator; } /** * Returns a filter callback for enumerating files */ public function getFilter(): Closure { $rootDirectory = $this->rootDirectory; $includePaths = $this->includePaths; $excludePaths = $this->excludePaths; return function (SplFileInfo $file) use ($rootDirectory, $includePaths, $excludePaths) { if ($file->getExtension() !== 'php') { return false; } $fullPath = $this->getFileFullPath($file); // Do not touch files that not under rootDirectory if (strpos($fullPath, $rootDirectory) !== 0) { return false; } if (!empty($includePaths)) { $found = false; foreach ($includePaths as $includePattern) { if (fnmatch("{$includePattern}*", $fullPath, FNM_NOESCAPE)) { $found = true; break; } } if (!$found) { return false; } } foreach ($excludePaths as $excludePattern) { if (fnmatch("{$excludePattern}*", $fullPath, FNM_NOESCAPE)) { return false; } } return true; }; } /** * Return the real path of the given file * * This is used for testing purpose with virtual file system. * In a vfs the 'realPath' method will always return false. * So we have a chance to mock this single function to return different path. */ protected function getFileFullPath(SplFileInfo $file): string { return $file->getRealPath(); } /** * Returns collection of directories to look at * * @throws UnexpectedValueException if directory not under the root */ private function getInPaths(): array { $inPaths = []; foreach ($this->includePaths as $path) { if (strpos($path, $this->rootDirectory, 0) === false) { throw new UnexpectedValueException(sprintf('Path %s is not in %s', $path, $this->rootDirectory)); } $inPaths[] = $path; } if (empty($inPaths)) { $inPaths[] = $this->rootDirectory; } return $inPaths; } /** * Returns the list of excluded paths */ private function getExcludePaths(): array { $excludePaths = []; foreach ($this->excludePaths as $path) { $path = str_replace('*', '.*', $path); $excludePaths[] = '#' . str_replace($this->rootDirectory . '/', '', $path) . '#'; } return $excludePaths; } } src/Instrument/PathResolver.php000064400000005224144760133060012641 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument; use function is_array; /** * Special class for resolving path for different file systems, wrappers, etc * * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath/4050444 * @see http://bugs.php.net/bug.php?id=52769 */ class PathResolver { /** * Custom replacement for realpath() and stream_resolve_include_path() * * @param string|array $somePath Path without normalization or array of paths * @param bool $shouldCheckExistence Flag for checking existence of resolved filename * * @return array|bool|string */ public static function realpath($somePath, $shouldCheckExistence = false) { // Do not resolve empty string/false/arrays into the current path if (!$somePath) { return $somePath; } if (is_array($somePath)) { return array_map([__CLASS__, __FUNCTION__], $somePath); } // Trick to get scheme name and path in one action. If no scheme, then there will be only one part $components = explode('://', $somePath, 2); [$pathScheme, $path] = isset($components[1]) ? $components : [null, $components[0]]; // Optimization to bypass complex logic for simple paths (eg. not in phar archives) if (!$pathScheme && ($fastPath = stream_resolve_include_path($somePath))) { return $fastPath; } $isRelative = !$pathScheme && ($path[0] !== '/') && ($path[1] !== ':'); if ($isRelative) { $path = getcwd() . DIRECTORY_SEPARATOR . $path; } // resolve path parts (single dot, double dot and double delimiters) $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); if (strpos($path, '.') !== false) { $parts = explode(DIRECTORY_SEPARATOR, $path); $absolutes = []; foreach ($parts as $part) { if ('.' === $part) { continue; } if ('..' === $part) { array_pop($absolutes); } else { $absolutes[] = $part; } } $path = implode(DIRECTORY_SEPARATOR, $absolutes); } if ($pathScheme) { $path = "{$pathScheme}://{$path}"; } if ($shouldCheckExistence && !file_exists($path)) { return false; } return $path; } } src/Instrument/Transformer/BaseSourceTransformer.php000064400000001673144760133060017007 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use Go\Core\AspectKernel; /** * Base source transformer class definition */ abstract class BaseSourceTransformer implements SourceTransformer { /** * Transformer options */ protected $options = []; /** * Aspect kernel instance */ protected $kernel; /** * Aspect container instance */ protected $container; /** * Default constructor for transformer */ public function __construct(AspectKernel $kernel, array $options = []) { $this->kernel = $kernel; $this->container = $kernel->getContainer(); $this->options = $options ?: $kernel->getOptions(); } } src/Instrument/Transformer/CachingTransformer.php000064400000011045144760133060016302 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use Closure; use Go\Core\AspectKernel; use Go\Instrument\ClassLoading\CachePathManager; use function dirname; /** * Caching transformer that is able to take the transformed source from a cache */ class CachingTransformer extends BaseSourceTransformer { /** * Mask of permission bits for cache files. * By default, permissions are affected by the umask system setting * * @var integer|null */ protected $cacheFileMode; /** * @var array|Closure|SourceTransformer[] */ protected $transformers = []; /** * Cache manager */ protected $cacheManager; /** * Class constructor * * @param array|callable $transformers Source transformers or callable that should return transformers */ public function __construct(AspectKernel $kernel, $transformers, CachePathManager $cacheManager) { parent::__construct($kernel); $this->cacheManager = $cacheManager; $this->cacheFileMode = $this->options['cacheFileMode']; $this->transformers = $transformers; } /** * This method may transform the supplied source and return a new replacement for it * * @return string See RESULT_XXX constants in the interface */ public function transform(StreamMetaData $metadata): string { $originalUri = $metadata->uri; $processingResult = self::RESULT_ABSTAIN; $cacheUri = $this->cacheManager->getCachePathForResource($originalUri); // Guard to disable overwriting of original files if ($cacheUri === $originalUri) { return self::RESULT_ABORTED; } $lastModified = filemtime($originalUri); $cacheState = $this->cacheManager->queryCacheState($originalUri); $cacheModified = $cacheState ? $cacheState['filemtime'] : 0; if ($cacheModified < $lastModified || (isset($cacheState['cacheUri']) && $cacheState['cacheUri'] !== $cacheUri) || !$this->container->isFresh($cacheModified) ) { $processingResult = $this->processTransformers($metadata); if ($processingResult === self::RESULT_TRANSFORMED) { $parentCacheDir = dirname($cacheUri); if (!is_dir($parentCacheDir)) { mkdir($parentCacheDir, $this->cacheFileMode, true); } file_put_contents($cacheUri, $metadata->source, LOCK_EX); // For cache files we don't want executable bits by default chmod($cacheUri, $this->cacheFileMode & (~0111)); } $this->cacheManager->setCacheState($originalUri, [ 'filemtime' => $_SERVER['REQUEST_TIME'] ?? time(), 'cacheUri' => ($processingResult === self::RESULT_TRANSFORMED) ? $cacheUri : null ]); return $processingResult; } if ($cacheState) { $processingResult = isset($cacheState['cacheUri']) ? self::RESULT_TRANSFORMED : self::RESULT_ABORTED; } if ($processingResult === self::RESULT_TRANSFORMED) { // Just replace all tokens in the stream $metadata->tokenStream = token_get_all(file_get_contents($cacheUri)); } return $processingResult; } /** * Iterates over transformers * * @return string See RESULT_XXX constants in the interface */ private function processTransformers(StreamMetaData $metadata): string { $overallResult = self::RESULT_ABSTAIN; if ($this->transformers instanceof Closure) { $delayedTransformers = $this->transformers; $this->transformers = $delayedTransformers(); } foreach ($this->transformers as $transformer) { $transformationResult = $transformer->transform($metadata); if ($overallResult === self::RESULT_ABSTAIN && $transformationResult === self::RESULT_TRANSFORMED) { $overallResult = self::RESULT_TRANSFORMED; } // transformer reported about termination, next transformers will be skipped if ($transformationResult === self::RESULT_ABORTED) { $overallResult = self::RESULT_ABORTED; break; } } return $overallResult; } } src/Instrument/Transformer/ConstructorExecutionTransformer.php000064400000010406144760133060021157 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use Go\Aop\Framework\ReflectionConstructorInvocation; use Go\Core\AspectContainer; use PhpParser\Node; use PhpParser\NodeTraverser; /** * Transforms the source code to add an ability to intercept new instances creation * * @see https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y * */ class ConstructorExecutionTransformer implements SourceTransformer { /** * List of constructor invocations per class * * @var array|ReflectionConstructorInvocation[] */ private static $constructorInvocationsCache = []; /** * Singletone */ public static function getInstance(): self { static $instance; if ($instance === null) { $instance = new static(); } return $instance; } /** * Rewrites all "new" expressions with our implementation * * @return string See RESULT_XXX constants in the interface */ public function transform(StreamMetaData $metadata): string { $newExpressionFinder = new NodeFinderVisitor([Node\Expr\New_::class]); // TODO: move this logic into walkSyntaxTree(Visitor $nodeVistor) method $traverser = new NodeTraverser(); $traverser->addVisitor($newExpressionFinder); $traverser->traverse($metadata->syntaxTree); /** @var Node\Expr\New_[] $newExpressions */ $newExpressions = $newExpressionFinder->getFoundNodes(); if (empty($newExpressions)) { return self::RESULT_ABSTAIN; } foreach ($newExpressions as $newExpressionNode) { $startPosition = $newExpressionNode->getAttribute('startTokenPos'); $metadata->tokenStream[$startPosition][1] = '\\' . __CLASS__ . '::getInstance()->{'; if ($metadata->tokenStream[$startPosition+1][0] === T_WHITESPACE) { unset($metadata->tokenStream[$startPosition+1]); } $isExplicitClass = $newExpressionNode->class instanceof Node\Name; $endClassNamePos = $newExpressionNode->class->getAttribute('endTokenPos'); $expressionSuffix = $isExplicitClass ? '::class}' : '}'; $metadata->tokenStream[$endClassNamePos][1] .= $expressionSuffix; } return self::RESULT_TRANSFORMED; } /** * Magic interceptor for instance creation * * @param string $className Name of the class to construct */ public function __get(string $className): object { return static::construct($className); } /** * Magic interceptor for instance creation * * @param string $className Name of the class to construct * @param array $args Arguments for the constructor */ public function __call(string $className, array $args): object { return static::construct($className, $args); } /** * Default implementation for accessing joinpoint or creating a new one on-fly */ protected static function construct(string $fullClassName, array $arguments = []): object { $fullClassName = ltrim($fullClassName, '\\'); if (!isset(self::$constructorInvocationsCache[$fullClassName])) { $invocation = null; $dynamicInit = AspectContainer::INIT_PREFIX . ':root'; try { $joinPointsRef = new \ReflectionProperty($fullClassName, '__joinPoints'); $joinPointsRef->setAccessible(true); $joinPoints = $joinPointsRef->getValue(); if (isset($joinPoints[$dynamicInit])) { $invocation = $joinPoints[$dynamicInit]; } } catch (\ReflectionException $e) { $invocation = null; } if (!$invocation) { $invocation = new ReflectionConstructorInvocation($fullClassName, 'root', []); } self::$constructorInvocationsCache[$fullClassName] = $invocation; } return self::$constructorInvocationsCache[$fullClassName]->__invoke($arguments); } } src/Instrument/Transformer/FilterInjectorTransformer.php000064400000010657144760133060017701 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use Go\Core\AspectKernel; use Go\Instrument\PathResolver; use Go\Instrument\ClassLoading\CachePathManager; use PhpParser\Node\Expr\Include_; use PhpParser\NodeTraverser; use RuntimeException; /** * Transformer that injects source filter for "require" and "include" operations */ class FilterInjectorTransformer implements SourceTransformer { /** * Php filter definition */ public const PHP_FILTER_READ = 'php://filter/read='; /** * Name of the filter to inject * * @var string */ protected static $filterName; /** * Kernel options * * @var array */ protected static $options = []; /** * @var AspectKernel|null */ protected static $kernel; /** * @var CachePathManager|null */ protected static $cachePathManager; /** * Class constructor */ public function __construct(AspectKernel $kernel, string $filterName, CachePathManager $cacheManager) { self::configure($kernel, $filterName, $cacheManager); } /** * Static configurator for filter */ protected static function configure(AspectKernel $kernel, string $filterName, CachePathManager $cacheManager): void { if (self::$kernel !== null) { throw new RuntimeException('Filter injector can be configured only once.'); } self::$kernel = $kernel; self::$options = $kernel->getOptions(); self::$filterName = $filterName; self::$cachePathManager = $cacheManager; } /** * Replace source path with correct one * * This operation can check for cache, can rewrite paths, add additional filters and much more * * @param string $originalResource Initial resource to include * @param string $originalDir Path to the directory from where include was called for resolving relative resources */ public static function rewrite($originalResource, string $originalDir = ''): string { static $appDir, $cacheDir, $debug; if ($appDir === null) { extract(self::$options, EXTR_IF_EXISTS); } $resource = (string) $originalResource; if ($resource[0] !== '/') { $shouldCheckExistence = true; $resource = PathResolver::realpath($resource, $shouldCheckExistence) ?: PathResolver::realpath("{$originalDir}/{$resource}", $shouldCheckExistence) ?: $originalResource; } $cachedResource = self::$cachePathManager->getCachePathForResource($resource); // If the cache is disabled or no cache yet, then use on-fly method if (!$cacheDir || $debug || !file_exists($cachedResource)) { return self::PHP_FILTER_READ . self::$filterName . '/resource=' . $resource; } return $cachedResource; } /** * Wrap all includes into rewrite filter * * @return string See RESULT_XXX constants in the interface */ public function transform(StreamMetaData $metadata): string { $includeExpressionFinder = new NodeFinderVisitor([Include_::class]); // TODO: move this logic into walkSyntaxTree(Visitor $nodeVistor) method $traverser = new NodeTraverser(); $traverser->addVisitor($includeExpressionFinder); $traverser->traverse($metadata->syntaxTree); /** @var Include_[] $includeExpressions */ $includeExpressions = $includeExpressionFinder->getFoundNodes(); if (empty($includeExpressions)) { return self::RESULT_ABSTAIN; } foreach ($includeExpressions as $includeExpression) { $startPosition = $includeExpression->getAttribute('startTokenPos'); $endPosition = $includeExpression->getAttribute('endTokenPos'); $metadata->tokenStream[$startPosition][1] .= ' \\' . __CLASS__ . '::rewrite('; if ($metadata->tokenStream[$startPosition+1][0] === T_WHITESPACE) { unset($metadata->tokenStream[$startPosition+1]); } $metadata->tokenStream[$endPosition][1] .= ', __DIR__)'; } return self::RESULT_TRANSFORMED; } } src/Instrument/Transformer/MagicConstantTransformer.php000064400000010462144760133060017502 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use Go\Core\AspectKernel; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Scalar\MagicConst; use PhpParser\Node\Scalar\MagicConst\Dir; use PhpParser\Node\Scalar\MagicConst\File; use PhpParser\NodeTraverser; use PhpParser\Node\Identifier; /** * Transformer that replaces magic __DIR__ and __FILE__ constants in the source code * * Additionally, ReflectionClass->getFileName() is also wrapped into normalizer method call */ class MagicConstantTransformer extends BaseSourceTransformer { /** * Root path of application * * @var string */ protected static $rootPath = ''; /** * Path to rewrite to (cache directory) * * @var string */ protected static $rewriteToPath = ''; /** * Class constructor */ public function __construct(AspectKernel $kernel) { parent::__construct($kernel); self::$rootPath = $this->options['appDir']; self::$rewriteToPath = $this->options['cacheDir']; } /** * This method may transform the supplied source and return a new replacement for it * * @return string See RESULT_XXX constants in the interface */ public function transform(StreamMetaData $metadata): string { $this->replaceMagicDirFileConstants($metadata); $this->wrapReflectionGetFileName($metadata); // We should always vote abstain, because if there is only changes for magic constants, we can drop them return self::RESULT_ABSTAIN; } /** * Resolves file name from the cache directory to the real application root dir */ public static function resolveFileName(string $fileName): string { $suffix = '.php'; $pathParts = explode($suffix, str_replace( [self::$rewriteToPath, DIRECTORY_SEPARATOR . '_proxies'], [self::$rootPath, ''], $fileName )); // throw away namespaced path from actual filename return $pathParts[0] . $suffix; } /** * Wraps all possible getFileName() methods from ReflectionFile */ private function wrapReflectionGetFileName(StreamMetaData $metadata): void { $methodCallFinder = new NodeFinderVisitor([MethodCall::class]); $traverser = new NodeTraverser(); $traverser->addVisitor($methodCallFinder); $traverser->traverse($metadata->syntaxTree); /** @var MethodCall[] $methodCalls */ $methodCalls = $methodCallFinder->getFoundNodes(); foreach ($methodCalls as $methodCallNode) { if (($methodCallNode->name instanceof Identifier) && ($methodCallNode->name->toString() === 'getFileName')) { $startPosition = $methodCallNode->getAttribute('startTokenPos'); $endPosition = $methodCallNode->getAttribute('endTokenPos'); $expressionPrefix = '\\' . __CLASS__ . '::resolveFileName('; $metadata->tokenStream[$startPosition][1] = $expressionPrefix . $metadata->tokenStream[$startPosition][1]; $metadata->tokenStream[$endPosition][1] .= ')'; } } } /** * Replaces all magic __DIR__ and __FILE__ constants in the file with calculated value */ private function replaceMagicDirFileConstants(StreamMetaData $metadata): void { $magicConstFinder = new NodeFinderVisitor([Dir::class, File::class]); $traverser = new NodeTraverser(); $traverser->addVisitor($magicConstFinder); $traverser->traverse($metadata->syntaxTree); /** @var MagicConst[] $magicConstants */ $magicConstants = $magicConstFinder->getFoundNodes(); $magicFileValue = $metadata->uri; $magicDirValue = dirname($magicFileValue); foreach ($magicConstants as $magicConstantNode) { $tokenPosition = $magicConstantNode->getAttribute('startTokenPos'); $replacement = $magicConstantNode instanceof Dir ? $magicDirValue : $magicFileValue; $metadata->tokenStream[$tokenPosition][1] = "'{$replacement}'"; } } } src/Instrument/Transformer/NodeFinderVisitor.php000064400000004400144760133060016115 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; /** * Node finder visitor for compatibility with PHP-Parser < 4.0 */ class NodeFinderVisitor extends NodeVisitorAbstract { /** * List of node class names to search * * @var string[] */ protected $searchNodes; /** * List of found nodes * * @var Node[] Found nodes */ protected $foundNodes; /** * Visitor constuctor * * @param string[] $searchNodes List of node names to search in AST */ public function __construct(array $searchNodes) { $this->searchNodes = $searchNodes; } /** * Get found nodes. * * Nodes are returned in pre-order. * * @return Node[] Found nodes */ public function getFoundNodes(): array { return $this->foundNodes; } /** * Called once before traversal. * * Return value semantics: * * null: $nodes stays as-is * * otherwise: $nodes is set to the return value * * @param Node[] $nodes Array of nodes * * @return null|Node[] Array of nodes */ public function beforeTraverse(array $nodes) { $this->foundNodes = []; return null; } /** * Called when entering a node. * * Return value semantics: * * null * => $node stays as-is * * NodeTraverser::DONT_TRAVERSE_CHILDREN * => Children of $node are not traversed. $node stays as-is * * NodeTraverser::STOP_TRAVERSAL * => Traversal is aborted. $node stays as-is * * otherwise * => $node is set to the return value * * @param Node $node Node * * @return null|int|Node Node */ public function enterNode(Node $node) { foreach ($this->searchNodes as $nodeClassName) { if ($node instanceof $nodeClassName) { $this->foundNodes[] = $node; } } return null; } } src/Instrument/Transformer/SelfValueTransformer.php000064400000003172144760133060016636 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use PhpParser\Node\Name\FullyQualified; use PhpParser\NodeTraverser; /** * Transformer that replaces `self` constants in the source code, e.g. new self() */ class SelfValueTransformer extends BaseSourceTransformer { /** * This method may transform the supplied source and return a new replacement for it * * @return string See RESULT_XXX constants in the interface */ public function transform(StreamMetaData $metadata): string { $selfValueVisitor = new SelfValueVisitor(); $traverser = new NodeTraverser(); $traverser->addVisitor($selfValueVisitor); $traverser->traverse($metadata->syntaxTree); $this->adjustSelfTokens($metadata, $selfValueVisitor->getReplacedNodes()); // We should always vote abstain, because if there are only changes for self we can drop them return self::RESULT_ABSTAIN; } /** * Adjusts tokens in the source code * * @param FullyQualified[] $replacedNodes Replaced nodes in the source code */ private function adjustSelfTokens(StreamMetaData $metadata, array $replacedNodes): void { foreach ($replacedNodes as $replacedNode) { $position = $replacedNode->getAttribute('startTokenPos'); $metadata->tokenStream[$position][1] = $replacedNode->toString(); } } } src/Instrument/Transformer/SelfValueVisitor.php000064400000007273144760133060016001 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use PhpParser\Node; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt; use PhpParser\Node\Expr; use PhpParser\NodeVisitorAbstract; /** * Node visitor that resolves class name for `self` nodes with FQN */ final class SelfValueVisitor extends NodeVisitorAbstract { /** * List of replaced nodes * * @var Node[] */ protected $replacedNodes = []; /** * Current namespace * * @var null|Name|string */ protected $namespace; /** * Current class name * * @var null|Name */ protected $className; /** * Returns list of changed `self` nodes * * @return Node[] */ public function getReplacedNodes(): array { return $this->replacedNodes; } /** * @inheritDoc */ public function beforeTraverse(array $nodes) { $this->namespace = null; $this->className = null; $this->replacedNodes = []; } /** * @inheritDoc */ public function enterNode(Node $node) { if ($node instanceof Stmt\Namespace_) { $this->namespace = $node->name->toString(); } elseif ($node instanceof Stmt\Class_) { if ($node->name !== null) { $this->className = new Name($node->name->toString()); } } elseif ($node instanceof Stmt\ClassMethod || $node instanceof Expr\Closure) { $node->returnType = $this->resolveType($node->returnType); } elseif ($node instanceof Node\Param) { $node->type = $this->resolveType($node->type); } elseif ( $node instanceof Expr\StaticCall || $node instanceof Expr\ClassConstFetch || $node instanceof Expr\New_ || $node instanceof Expr\Instanceof_ ) { if ($node->class instanceof Name) { $node->class = $this->resolveClassName($node->class); } } elseif ($node instanceof Stmt\Catch_) { foreach ($node->types as &$type) { $type = $this->resolveClassName($type); } } } /** * Resolves `self` class name with value * * @param Name $name Instance of original node * * @return Name|FullyQualified */ protected function resolveClassName(Name $name): Name { // Skip all names except special `self` if (strtolower($name->toString()) !== 'self') { return $name; } // Save the original name $originalName = $name; $name = clone $originalName; $name->setAttribute('originalName', $originalName); $fullClassName = Name::concat($this->namespace, $this->className); $resolvedSelfName = new FullyQualified('\\' . ltrim($fullClassName->toString(), '\\'), $name->getAttributes()); $this->replacedNodes[] = $resolvedSelfName; return $resolvedSelfName; } /** * Helper method for resolving type nodes * * @param Node|string|null $node Instance of node * * @return Node|Name|FullyQualified */ private function resolveType($node) { if ($node instanceof Node\NullableType) { $node->type = $this->resolveType($node->type); return $node; } if ($node instanceof Name) { return $this->resolveClassName($node); } return $node; } } src/Instrument/Transformer/SourceTransformer.php000064400000002073144760133060016207 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; /** * General source transformer interface */ interface SourceTransformer { /** * Transformer decided to stop whole transformation process, all changes should be reverted */ public const RESULT_ABORTED = 'aborted'; /** * Transformer voted to abstain transformation, need to process following transformers to get result */ public const RESULT_ABSTAIN = 'abstain'; /** * Source code was transformed, can process next transformers if needed */ public const RESULT_TRANSFORMED = 'transformed'; /** * This method may transform the supplied source and return a new replacement for it * * @return string See RESULT_XXX constants in the interface */ public function transform(StreamMetaData $metadata): string; } src/Instrument/Transformer/StreamMetaData.php000064400000007646144760133060015373 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use Go\Instrument\PathResolver; use Go\ParserReflection\ReflectionEngine; use InvalidArgumentException; use PhpParser\Node; use function is_array, is_resource; /** * Stream metadata object * * @property-read string $source */ class StreamMetaData { /** * Mapping between array keys and properties * * @var array */ private static $propertyMap = [ 'stream_type' => 'streamType', 'wrapper_type' => 'wrapperType', 'wrapper_data' => 'wrapperData', 'filters' => 'filterList', 'uri' => 'uri', ]; /** * A label describing the underlying implementation of the stream. * * @var string */ public $streamType; /** * A label describing the protocol wrapper implementation layered over the stream. * * @var string */ public $wrapperType; /** * Wrapper-specific data attached to this stream. * * @var mixed */ public $wrapperData; /** * Array containing the names of any filters that have been stacked onto this stream. * * @var array */ public $filterList; /** * The URI/filename associated with this stream. * * @var string */ public $uri; /** * Information about syntax tree * * @var Node[] */ public $syntaxTree; /** * List of source tokens * * @var array */ public $tokenStream = []; /** * Creates metadata object from stream * * @param resource $stream Instance of stream * @param string $source Source code or null * @throws \InvalidArgumentException for invalid stream */ public function __construct($stream, string $source = null) { if (!is_resource($stream)) { throw new InvalidArgumentException('Stream should be valid resource'); } $metadata = stream_get_meta_data($stream); if (preg_match('/resource=(.+)$/', $metadata['uri'], $matches)) { $metadata['uri'] = PathResolver::realpath($matches[1]); } foreach ($metadata as $key => $value) { if (!isset(self::$propertyMap[$key])) { continue; } $mappedKey = self::$propertyMap[$key]; $this->$mappedKey = $value; } $this->syntaxTree = ReflectionEngine::parseFile($this->uri, $source); $this->setSource($source); } /** * @inheritDoc */ public function __get($name) { if ($name === 'source') { return $this->getSource(); } return null; } /** * @inheritDoc */ public function __set($name, $value) { if ($name === 'source') { trigger_error('Setting StreamMetaData->source is deprecated, use tokenStream instead', E_USER_DEPRECATED); $this->setSource($value); } } /** * Returns source code directly from tokens */ private function getSource(): string { $transformedSource = ''; foreach ($this->tokenStream as $token) { $transformedSource .= $token[1] ?? $token; } return $transformedSource; } /** * Sets the new source for this file * * @TODO: Unfortunately, AST won't be changed, so please be accurate during transformation * * @param string $newSource */ private function setSource(string $newSource): void { $rawTokens = token_get_all($newSource); foreach ($rawTokens as $index => $rawToken) { $this->tokenStream[$index] = is_array($rawToken) ? $rawToken : [T_STRING, $rawToken]; } } } src/Instrument/Transformer/WeavingTransformer.php000064400000027522144760133060016355 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Instrument\Transformer; use Go\Aop\Advisor; use Go\Aop\Aspect; use Go\Aop\Features; use Go\Aop\Framework\AbstractJoinpoint; use Go\Core\AdviceMatcher; use Go\Core\AspectContainer; use Go\Core\AspectKernel; use Go\Core\AspectLoader; use Go\Instrument\ClassLoading\CachePathManager; use Go\ParserReflection\ReflectionClass; use Go\ParserReflection\ReflectionFile; use Go\ParserReflection\ReflectionFileNamespace; use Go\ParserReflection\ReflectionMethod; use Go\Proxy\ClassProxyGenerator; use Go\Proxy\FunctionProxyGenerator; use Go\Proxy\TraitProxyGenerator; /** * Main transformer that performs weaving of aspects into the source code */ class WeavingTransformer extends BaseSourceTransformer { /** * Advice matcher for class */ protected $adviceMatcher; /** * Should we use parameter widening for our decorators */ protected $useParameterWidening = false; /** * Cache manager */ private $cachePathManager; /** * Loader for aspects */ protected $aspectLoader; /** * Constructs a weaving transformer */ public function __construct( AspectKernel $kernel, AdviceMatcher $adviceMatcher, CachePathManager $cachePathManager, AspectLoader $loader ) { parent::__construct($kernel); $this->adviceMatcher = $adviceMatcher; $this->cachePathManager = $cachePathManager; $this->aspectLoader = $loader; $this->useParameterWidening = $kernel->hasFeature(Features::PARAMETER_WIDENING); } /** * This method may transform the supplied source and return a new replacement for it * * @param StreamMetaData $metadata * @return string See RESULT_XXX constants in the interface */ public function transform(StreamMetaData $metadata): string { $totalTransformations = 0; $parsedSource = new ReflectionFile($metadata->uri, $metadata->syntaxTree); // Check if we have some new aspects that weren't loaded yet $unloadedAspects = $this->aspectLoader->getUnloadedAspects(); if (!empty($unloadedAspects)) { $this->loadAndRegisterAspects($unloadedAspects); } $advisors = $this->container->getByTag('advisor'); $namespaces = $parsedSource->getFileNamespaces(); foreach ($namespaces as $namespace) { $classes = $namespace->getClasses(); foreach ($classes as $class) { // Skip interfaces and aspects if ($class->isInterface() || in_array(Aspect::class, $class->getInterfaceNames(), true)) { continue; } $wasClassProcessed = $this->processSingleClass( $advisors, $metadata, $class, $parsedSource->isStrictMode() ); $totalTransformations += (integer) $wasClassProcessed; } $wasFunctionsProcessed = $this->processFunctions($advisors, $metadata, $namespace); $totalTransformations += (integer) $wasFunctionsProcessed; } $result = ($totalTransformations > 0) ? self::RESULT_TRANSFORMED : self::RESULT_ABSTAIN; return $result; } /** * Performs weaving of single class if needed, returns true if the class was processed * * @param Advisor[] $advisors List of advisors * @param StreamMetaData $metadata * @param ReflectionClass $class * @param bool $useStrictMode If the source file used strict mode, the proxy should too * @return bool */ private function processSingleClass( array $advisors, StreamMetaData $metadata, ReflectionClass $class, bool $useStrictMode ): bool { $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors); if (empty($advices)) { // Fast return if there aren't any advices for that class return false; } // Sort advices in advance to keep the correct order in cache, and leave only keys for the cache $advices = AbstractJoinpoint::flatAndSortAdvices($advices); // Prepare new class name $newClassName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX; // Replace original class name with new $this->adjustOriginalClass($class, $advices, $metadata, $newClassName); $newParentName = $class->getNamespaceName() . '\\' . $newClassName; // Prepare child Aop proxy $childProxyGenerator = $class->isTrait() ? new TraitProxyGenerator($class, $newParentName, $advices, $this->useParameterWidening) : new ClassProxyGenerator($class, $newParentName, $advices, $this->useParameterWidening); $refNamespace = new ReflectionFileNamespace($class->getFileName(), $class->getNamespaceName()); foreach ($refNamespace->getNamespaceAliases() as $fqdn => $alias) { // Either we have a string or Identifier node if ($alias !== null) { $childProxyGenerator->addUse($fqdn, (string) $alias); } else { $childProxyGenerator->addUse($fqdn); } } $childCode = $childProxyGenerator->generate(); if ($useStrictMode) { $childCode = 'declare(strict_types=1);' . PHP_EOL . $childCode; } $contentToInclude = $this->saveProxyToCache($class, $childCode); // Get last token for this class $lastClassToken = $class->getNode()->getAttribute('endTokenPos'); $metadata->tokenStream[$lastClassToken][1] .= PHP_EOL . $contentToInclude; return true; } /** * Adjust definition of original class source to enable extending * * @param array $advices List of class advices (used to check for final methods and make them non-final) */ private function adjustOriginalClass( ReflectionClass $class, array $advices, StreamMetaData $streamMetaData, string $newClassName ): void { $classNode = $class->getNode(); $position = $classNode->getAttribute('startTokenPos'); do { if (isset($streamMetaData->tokenStream[$position])) { $token = $streamMetaData->tokenStream[$position]; // Remove final and following whitespace from the class, child will be final instead if ($token[0] === T_FINAL) { unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]); } // First string is class/trait name if ($token[0] === T_STRING) { $streamMetaData->tokenStream[$position][1] = $newClassName; // We have finished our job, can break this loop break; } } ++$position; } while (true); foreach ($class->getMethods(ReflectionMethod::IS_FINAL) as $finalMethod) { if (!$finalMethod instanceof ReflectionMethod || $finalMethod->getDeclaringClass()->name !== $class->name) { continue; } $hasDynamicAdvice = isset($advices[AspectContainer::METHOD_PREFIX][$finalMethod->name]); $hasStaticAdvice = isset($advices[AspectContainer::STATIC_METHOD_PREFIX][$finalMethod->name]); if (!$hasDynamicAdvice && !$hasStaticAdvice) { continue; } $methodNode = $finalMethod->getNode(); $position = $methodNode->getAttribute('startTokenPos'); do { if (isset($streamMetaData->tokenStream[$position])) { $token = $streamMetaData->tokenStream[$position]; // Remove final and following whitespace from the method, child will be final instead if ($token[0] === T_FINAL) { unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]); break; } } ++$position; } while (true); } } /** * Performs weaving of functions in the current namespace, returns true if functions were processed, false otherwise * * @param Advisor[] $advisors List of advisors */ private function processFunctions( array $advisors, StreamMetaData $metadata, ReflectionFileNamespace $namespace ): bool { static $cacheDirSuffix = '/_functions/'; $wasProcessedFunctions = false; $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors); $cacheDir = $this->cachePathManager->getCacheDir(); if (!empty($functionAdvices)) { $cacheDir .= $cacheDirSuffix; $fileName = str_replace('\\', '/', $namespace->getName()) . '.php'; $functionFileName = $cacheDir . $fileName; if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) { $functionAdvices = AbstractJoinpoint::flatAndSortAdvices($functionAdvices); $dirname = dirname($functionFileName); if (!file_exists($dirname)) { mkdir($dirname, $this->options['cacheFileMode'], true); } $generator = new FunctionProxyGenerator($namespace, $functionAdvices, $this->useParameterWidening); file_put_contents($functionFileName, $generator->generate(), LOCK_EX); // For cache files we don't want executable bits by default chmod($functionFileName, $this->options['cacheFileMode'] & (~0111)); } $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';'; $lastTokenPosition = $namespace->getLastTokenPosition(); $metadata->tokenStream[$lastTokenPosition][1] .= PHP_EOL . $content; $wasProcessedFunctions = true; } return $wasProcessedFunctions; } /** * Save AOP proxy to the separate file anr returns the php source code for inclusion */ private function saveProxyToCache(ReflectionClass $class, string $childCode): string { static $cacheDirSuffix = '/_proxies/'; $cacheDir = $this->cachePathManager->getCacheDir() . $cacheDirSuffix; $relativePath = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName()); $proxyRelativePath = str_replace('\\', '/', $relativePath . '/' . $class->getName() . '.php'); $proxyFileName = $cacheDir . $proxyRelativePath; $dirname = dirname($proxyFileName); if (!file_exists($dirname)) { mkdir($dirname, $this->options['cacheFileMode'], true); } $body = 'options['cacheFileMode'] & (~0111)); return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $proxyRelativePath, true) . ';'; } /** * Utility method to load and register unloaded aspects * * @param array $unloadedAspects List of unloaded aspects */ private function loadAndRegisterAspects(array $unloadedAspects): void { foreach ($unloadedAspects as $unloadedAspect) { $this->aspectLoader->loadAndRegister($unloadedAspect); } } } src/Lang/Annotation/After.php000064400000000637144760133060012112 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * After advice annotation * * @Annotation * @Target({ "METHOD", "PROPERTY" }) */ class After extends BaseInterceptor { } src/Lang/Annotation/AfterThrowing.php000064400000000640144760133060013626 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * After throwing advice annotation * * @Annotation * @Target("METHOD") */ class AfterThrowing extends BaseInterceptor { } src/Lang/Annotation/Around.php000064400000000641144760133060012274 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * Around advice annotation * * @Annotation * @Target({ "METHOD", "PROPERTY" }) */ class Around extends BaseInterceptor { } src/Lang/Annotation/Aspect.php000064400000000631144760133060012262 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * Annotation marker class for aspect * * @Annotation * @Target("CLASS") */ class Aspect extends BaseAnnotation { } src/Lang/Annotation/BaseAnnotation.php000064400000002651144760133060013754 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * @Annotation */ abstract class BaseAnnotation { /** * Value property. Common among all derived classes. * * @var string */ public $value; /** * Constructor * * @param array $data Key-value for properties to be defined in this class */ final public function __construct(array $data) { foreach ($data as $key => $value) { $this->$key = $value; } } /** * Error handler for unknown property accessor in Annotation class. * * @param string $name Unknown property name */ public function __get($name) { throw new \BadMethodCallException( sprintf("Unknown property '%s' on annotation '%s'.", $name, get_class($this)) ); } /** * Error handler for unknown property mutator in Annotation class. * * @param string $name Unknown property name * @param mixed $value Property value */ public function __set($name, $value) { throw new \BadMethodCallException( sprintf("Unknown property '%s' on annotation '%s'.", $name, get_class($this)) ); } } src/Lang/Annotation/BaseInterceptor.php000064400000000772144760133060014142 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * Default interceptor class with common attributes */ class BaseInterceptor extends BaseAnnotation implements Interceptor { /** * Order for advice * * @var integer */ public $order = 0; } src/Lang/Annotation/Before.php000064400000000641144760133060012246 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * Before advice annotation * * @Annotation * @Target({ "METHOD", "PROPERTY" }) */ class Before extends BaseInterceptor { } src/Lang/Annotation/DeclareError.php000064400000001202144760133060013407 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * Declare error annotation * * @Annotation * @Target("PROPERTY") * * @Attributes({ * @Attribute("value", type = "string", required=true), * @Attribute("level", type = "integer"), * }) */ class DeclareError extends BaseAnnotation { /** * Interface name to add * * @var integer */ public $level = E_USER_NOTICE; } src/Lang/Annotation/DeclareParents.php000064400000001433144760133060013740 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * Declare parents annotation * * @Annotation * @Target("PROPERTY") * * @Attributes({ * @Attribute("value", type = "string", required=true), * @Attribute("interface", type = "string"), * @Attribute("defaultImpl", type = "string") * }) */ class DeclareParents extends BaseAnnotation { /** * Default implementation (trait name) * * @var string */ public $defaultImpl; /** * Interface name to add * * @var string */ public $interface; } src/Lang/Annotation/Interceptor.php000064400000000540144760133060013340 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * Interceptor annotation marker */ interface Interceptor { } src/Lang/Annotation/Pointcut.php000064400000000740144760133060012651 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Lang\Annotation; /** * Pointcut annotation * * @Annotation * @Target("METHOD") * * @Attributes({ * @Attribute("value", type = "string", required=true) * }) */ class Pointcut extends BaseAnnotation { } src/Proxy/ClassProxyGenerator.php000064400000021767144760133060013164 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy; use Go\Aop\Advice; use Go\Aop\Framework\ClassFieldAccess; use Go\Aop\Framework\DynamicClosureMethodInvocation; use Go\Aop\Framework\ReflectionConstructorInvocation; use Go\Aop\Framework\StaticClosureMethodInvocation; use Go\Aop\Framework\StaticInitializationJoinpoint; use Go\Aop\Intercept\Joinpoint; use Go\Aop\Proxy; use Go\Core\AspectContainer; use Go\Core\AspectKernel; use Go\Core\LazyAdvisorAccessor; use Go\Proxy\Part\FunctionCallArgumentListGenerator; use Go\Proxy\Part\InterceptedConstructorGenerator; use Go\Proxy\Part\InterceptedMethodGenerator; use Go\Proxy\Part\JoinPointPropertyGenerator; use Go\Proxy\Part\PropertyInterceptionTrait; use ReflectionClass; use ReflectionMethod; use Zend\Code\Generator\ClassGenerator; use Zend\Code\Generator\DocBlockGenerator; use Zend\Code\Reflection\DocBlockReflection; /** * Class proxy builder that is used to generate a child class from the list of joinpoints */ class ClassProxyGenerator { /** * Static mappings for class name for excluding if..else check */ protected static $invocationClassMap = [ AspectContainer::METHOD_PREFIX => DynamicClosureMethodInvocation::class, AspectContainer::STATIC_METHOD_PREFIX => StaticClosureMethodInvocation::class, AspectContainer::PROPERTY_PREFIX => ClassFieldAccess::class, AspectContainer::STATIC_INIT_PREFIX => StaticInitializationJoinpoint::class, AspectContainer::INIT_PREFIX => ReflectionConstructorInvocation::class ]; /** * List of advices that are used for generation of child */ protected $advices = []; /** * Instance of class generator */ protected $generator; /** * Generates an child code by original class reflection and joinpoints for it * * @param ReflectionClass $originalClass Original class reflection * @param string $parentClassName Parent class name to use * @param string[][] $classAdvices List of advices for class * @param bool $useParameterWidening Enables usage of parameter widening feature */ public function __construct( ReflectionClass $originalClass, string $parentClassName, array $classAdvices, bool $useParameterWidening ) { $this->advices = $classAdvices; $dynamicMethodAdvices = $classAdvices[AspectContainer::METHOD_PREFIX] ?? []; $staticMethodAdvices = $classAdvices[AspectContainer::STATIC_METHOD_PREFIX] ?? []; $propertyAdvices = $classAdvices[AspectContainer::PROPERTY_PREFIX] ?? []; $interceptedMethods = array_keys($dynamicMethodAdvices + $staticMethodAdvices); $interceptedProperties = array_keys($propertyAdvices); $introducedInterfaces = $classAdvices[AspectContainer::INTRODUCTION_INTERFACE_PREFIX] ?? []; $introducedTraits = $classAdvices[AspectContainer::INTRODUCTION_TRAIT_PREFIX] ?? []; $generatedProperties = [new JoinPointPropertyGenerator($classAdvices)]; $generatedMethods = $this->interceptMethods($originalClass, $interceptedMethods); $introducedInterfaces[] = '\\' . Proxy::class; if (!empty($interceptedProperties)) { $generatedMethods['__construct'] = new InterceptedConstructorGenerator( $interceptedProperties, $originalClass->getConstructor(), $generatedMethods['__construct'] ?? null, $useParameterWidening ); $introducedTraits[] = '\\' . PropertyInterceptionTrait::class; } $this->generator = new ClassGenerator( $originalClass->getShortName(), $originalClass->getNamespaceName(), $originalClass->isFinal() ? ClassGenerator::FLAG_FINAL : null, $parentClassName, $introducedInterfaces, $generatedProperties, $generatedMethods ); if ($originalClass->getDocComment()) { $reflectionDocBlock = new DocBlockReflection($originalClass->getDocComment()); $this->generator->setDocBlock(DocBlockGenerator::fromReflection($reflectionDocBlock)); } $this->generator->addTraits($introducedTraits); } /** * Adds use alias for this class */ public function addUse(string $use, string $useAlias = null): void { $this->generator->addUse($use, $useAlias); } /** * Inject advices into given class * * NB This method will be used as a callback during source code evaluation to inject joinpoints */ public static function injectJoinPoints(string $targetClassName): void { $reflectionClass = new ReflectionClass($targetClassName); $joinPointsProperty = $reflectionClass->getProperty(JoinPointPropertyGenerator::NAME); $joinPointsProperty->setAccessible(true); $advices = $joinPointsProperty->getValue(); $joinPoints = static::wrapWithJoinPoints($advices, $reflectionClass->getParentClass()->name); $joinPointsProperty->setValue($joinPoints); $staticInit = AspectContainer::STATIC_INIT_PREFIX . ':root'; if (isset($joinPoints[$staticInit])) { $joinPoints[$staticInit]->__invoke(); } } /** * Generates the source code of child class */ public function generate(): string { $classCode = $this->generator->generate(); return $classCode // Inject advices on call . '\\' . __CLASS__ . '::injectJoinPoints(' . $this->generator->getName() . '::class);'; } /** * Wrap advices with joinpoint object * * @param array|Advice[][][] $classAdvices Advices for specific class * * @throws \UnexpectedValueException If joinPoint type is unknown * * NB: Extension should be responsible for wrapping advice with join point. * * @return Joinpoint[] returns list of joinpoint ready to use */ protected static function wrapWithJoinPoints(array $classAdvices, string $className): array { /** @var LazyAdvisorAccessor $accessor */ static $accessor; if (!isset($accessor)) { $aspectKernel = AspectKernel::getInstance(); $accessor = $aspectKernel->getContainer()->get('aspect.advisor.accessor'); } $joinPoints = []; foreach ($classAdvices as $joinPointType => $typedAdvices) { // if not isset then we don't want to create such invocation for class if (!isset(self::$invocationClassMap[$joinPointType])) { continue; } foreach ($typedAdvices as $joinPointName => $advices) { $filledAdvices = []; foreach ($advices as $advisorName) { $filledAdvices[] = $accessor->$advisorName; } $joinpoint = new self::$invocationClassMap[$joinPointType]($className, $joinPointName, $filledAdvices); $joinPoints["$joinPointType:$joinPointName"] = $joinpoint; } } return $joinPoints; } /** * Returns list of intercepted method generators for class by method names * * @param string[] $methodNames List of methods to intercept * * @return InterceptedMethodGenerator[] */ protected function interceptMethods(ReflectionClass $originalClass, array $methodNames): array { $interceptedMethods = []; foreach ($methodNames as $methodName) { $reflectionMethod = $originalClass->getMethod($methodName); $methodBody = $this->getJoinpointInvocationBody($reflectionMethod); $interceptedMethods[$methodName] = new InterceptedMethodGenerator($reflectionMethod, $methodBody); } return $interceptedMethods; } /** * Creates string definition for method body by method reflection */ protected function getJoinpointInvocationBody(ReflectionMethod $method): string { $isStatic = $method->isStatic(); $scope = $isStatic ? 'static::class' : '$this'; $prefix = $isStatic ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX; $argumentList = new FunctionCallArgumentListGenerator($method); $argumentCode = $argumentList->generate(); $return = 'return '; if ($method->hasReturnType()) { $returnType = (string) $method->getReturnType(); if ($returnType === 'void') { // void return types should not return anything $return = ''; } } if (!empty($argumentCode)) { $scope = "$scope, $argumentCode"; } $body = "{$return}self::\$__joinPoints['{$prefix}:{$method->name}']->__invoke($scope);"; return $body; } } src/Proxy/FunctionProxyGenerator.php000064400000010426144760133060013672 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy; use Go\Aop\Framework\ReflectionFunctionInvocation; use Go\Aop\Intercept\FunctionInvocation; use Go\Core\AspectContainer; use Go\Core\AspectKernel; use Go\ParserReflection\ReflectionFileNamespace; use Go\Proxy\Part\FunctionCallArgumentListGenerator; use Go\Proxy\Part\InterceptedFunctionGenerator; use ReflectionFunction; use Zend\Code\Generator\FileGenerator; use Zend\Code\Generator\ValueGenerator; /** * Function proxy builder that is used to generate a proxy-function from the list of joinpoints */ class FunctionProxyGenerator { /** * List of advices that are used for generation of child */ protected $advices = []; /** * Instance of file generator */ protected $fileGenerator; /** * Constructs functions stub class from namespace Reflection * * @param ReflectionFileNamespace $namespace Reflection of namespace * @param string[][] $advices List of function advices * @param bool $useParameterWidening Enables usage of parameter widening feature * * @throws \ReflectionException If there is an advice for unknown function */ public function __construct( ReflectionFileNamespace $namespace, array $advices = [], bool $useParameterWidening = false ) { $this->advices = $advices; $this->fileGenerator = new FileGenerator(); $this->fileGenerator->setNamespace($namespace->getName()); $functionsContent = []; $functionAdvices = $advices[AspectContainer::FUNCTION_PREFIX] ?? []; foreach (array_keys($functionAdvices) as $functionName) { $functionReflection = new ReflectionFunction($functionName); $functionBody = $this->getJoinpointInvocationBody($functionReflection); $interceptedFunction = new InterceptedFunctionGenerator($functionReflection, $functionBody, $useParameterWidening); $functionsContent[] = $interceptedFunction->generate(); } $this->fileGenerator->setBody(implode("\n", $functionsContent)); } /** * Returns a joinpoint for specific function in the namespace * * @param array $advices List of advices */ public static function getJoinPoint(string $functionName, array $advices): FunctionInvocation { static $accessor; if ($accessor === null) { $accessor = AspectKernel::getInstance()->getContainer()->get('aspect.advisor.accessor'); } $filledAdvices = []; foreach ($advices as $advisorName) { $filledAdvices[] = $accessor->$advisorName; } return new ReflectionFunctionInvocation($functionName, $filledAdvices); } /** * Generates the source code of function proxies in given namespace */ public function generate(): string { return $this->fileGenerator->generate(); } /** * Creates string definition for function method body by function reflection */ protected function getJoinpointInvocationBody(ReflectionFunction $function): string { $class = '\\' . __CLASS__; $argumentList = new FunctionCallArgumentListGenerator($function); $argumentCode = $argumentList->generate(); $return = 'return '; if ($function->hasReturnType()) { $returnType = (string) $function->getReturnType(); if ($returnType === 'void') { // void return types should not return anything $return = ''; } } $functionAdvices = $this->advices[AspectContainer::FUNCTION_PREFIX][$function->name]; $advicesArray = new ValueGenerator($functionAdvices, ValueGenerator::TYPE_ARRAY_SHORT); $advicesArray->setArrayDepth(1); $advicesCode = $advicesArray->generate(); return <<name}', {$advicesCode}); } {$return}\$__joinPoint->__invoke($argumentCode); BODY; } } src/Proxy/Part/FunctionCallArgumentListGenerator.php000064400000004302144760133060016665 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use ReflectionFunctionAbstract; use Zend\Code\Generator\AbstractGenerator; /** * Prepares the function call argument list */ final class FunctionCallArgumentListGenerator extends AbstractGenerator { /** * List of function arguments * * @var string[] */ private $arguments; /** * If function contains optional arguments */ private $hasOptionals = false; /** * Definition of variadic argument or null if function is not variadic * * @var mixed|null */ private $variadicArgument = null; /** * FunctionCallArgumentList constructor. * * @param ReflectionFunctionAbstract $functionLike Instance of function or method to call */ public function __construct(ReflectionFunctionAbstract $functionLike) { parent::__construct(); foreach ($functionLike->getParameters() as $parameter) { $byReference = ($parameter->isPassedByReference() && !$parameter->isVariadic()) ? '&' : ''; $this->hasOptionals = $this->hasOptionals || $parameter->isOptional(); $this->arguments[] = $byReference . '$' . $parameter->name; } if ($functionLike->isVariadic()) { // Variadic argument is last and should be handled separately $this->variadicArgument = array_pop($this->arguments); } } /** * @inheritDoc */ public function generate() { $argumentsPart = []; if ($this->variadicArgument !== null) { $argumentsPart[] = $this->variadicArgument; } if (!empty($this->arguments)) { $argumentLine = '[' . implode(', ', $this->arguments) . ']'; if ($this->hasOptionals) { $argumentLine = "\\array_slice($argumentLine, 0, \\func_num_args())"; } array_unshift($argumentsPart, $argumentLine); } return implode(', ', $argumentsPart); } } src/Proxy/Part/FunctionParameterList.php000064400000004265144760133060014370 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use ReflectionFunctionAbstract; use Zend\Code\Generator\ParameterGenerator; use Zend\Code\Generator\ValueGenerator; /** * Generates parameters from reflection definition */ final class FunctionParameterList { /** * @var ParameterGenerator[] */ private $generatedParameters = []; /** * ParameterListGenerator constructor. * * @param ReflectionFunctionAbstract $functionLike Instance of function or method * @param bool $useTypeWidening Should generated parameters use type widening */ public function __construct(ReflectionFunctionAbstract $functionLike, bool $useTypeWidening = false) { $reflectionParameters = $functionLike->getParameters(); foreach ($reflectionParameters as $reflectionParameter) { $defaultValue = null; $isDefaultValueAvailable = $reflectionParameter->isDefaultValueAvailable(); if ($isDefaultValueAvailable) { $defaultValue = new ValueGenerator($reflectionParameter->getDefaultValue()); } elseif ($reflectionParameter->isOptional() && !$reflectionParameter->isVariadic()) { $defaultValue = new ValueGenerator(null); } $generatedParameter = new ParameterGenerator( $reflectionParameter->getName(), $useTypeWidening ? '' : $reflectionParameter->getType(), $defaultValue, $reflectionParameter->getPosition(), $reflectionParameter->isPassedByReference() ); $generatedParameter->setVariadic($reflectionParameter->isVariadic()); $this->generatedParameters[] = $generatedParameter; } } /** * Returns the list of generated parameters * * @return ParameterGenerator[] */ public function getGeneratedParameters(): array { return $this->generatedParameters; } } src/Proxy/Part/InterceptedConstructorGenerator.php000064400000007224144760133060016467 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use LogicException; use ReflectionMethod; use Zend\Code\Generator\MethodGenerator; use function count; /** * Prepares the definition of intercepted constructor */ final class InterceptedConstructorGenerator extends MethodGenerator { /** * Constructor generator * * @var MethodGenerator */ private $generatedConstructor; /** * InterceptedConstructor * * @param array $interceptedProperties List of intercepted properties for the class * @param ReflectionMethod $constructor Instance of original constructor or null * @param MethodGenerator $generatedConstructor Constructor body generator (if present) * @param bool $useTypeWidening Should generator use parameter widening for PHP>=7.2 * * @throws LogicException if constructor is private */ public function __construct( array $interceptedProperties, ReflectionMethod $constructor = null, MethodGenerator $generatedConstructor = null, bool $useTypeWidening = false ) { $constructorBody = count($interceptedProperties) > 0 ? $this->getConstructorBody($interceptedProperties) : ''; if ($constructor !== null && $constructor->isPrivate()) { throw new LogicException( "Constructor in the class {$constructor->class} is declared as private. " . 'Properties could not be intercepted.' ); } if ($constructor !== null) { if ($generatedConstructor === null) { $callArguments = new FunctionCallArgumentListGenerator($constructor); $splatPrefix = $constructor->getNumberOfParameters() > 0 ? '...' : ''; $parentCallBody = 'parent::__construct(' . $splatPrefix . $callArguments->generate() . ');'; $generatedConstructor = new InterceptedMethodGenerator($constructor, $parentCallBody, $useTypeWidening); } $constructorBody .= PHP_EOL . $generatedConstructor->getBody(); $generatedConstructor->setBody($constructorBody); } else { $generatedConstructor = new MethodGenerator('__construct', [], [], $constructorBody); } $this->generatedConstructor = $generatedConstructor; } /** * @inheritdoc */ public function generate() { return $this->generatedConstructor->generate(); } /** * Returns constructor code * * @param array $interceptedProperties List of properties to intercept */ private function getConstructorBody(array $interceptedProperties): string { $assocProperties = []; $listProperties = []; foreach ($interceptedProperties as $propertyName) { $assocProperties[] = " '{$propertyName}' => &\$target->{$propertyName}"; $listProperties[] = " \$target->{$propertyName}"; } $lines = [ '$accessor = function(array &$propertyStorage, object $target) {', ' $propertyStorage = [', implode(',' . PHP_EOL, $assocProperties), ' ];', ' unset(', implode(',' . PHP_EOL, $listProperties), ' );', '};', '($accessor->bindTo($this, parent::class))($this->__properties, $this);' ]; return implode(PHP_EOL, $lines); } } src/Proxy/Part/InterceptedFunctionGenerator.php000064400000006550144760133060015730 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use ReflectionFunction; use Zend\Code\Generator\AbstractGenerator; use Zend\Code\Generator\DocBlockGenerator; use Zend\Code\Generator\ParameterGenerator; use Zend\Code\Generator\TypeGenerator; use Zend\Code\Reflection\DocBlockReflection; /** * Prepares the definition of intercepted function */ final class InterceptedFunctionGenerator extends AbstractGenerator { /** * @var string */ protected $name; /** * @var DocBlockGenerator|null */ protected $docBlock; /** * @var ParameterGenerator[] */ protected $parameters = []; /** * @var string */ protected $body; /** * @var null|TypeGenerator */ private $returnType; /** * @var bool */ private $returnsReference; /** * InterceptedMethod constructor. * * @param ReflectionFunction $reflectionFunction Instance of original method * @param string $body Method body * @param bool $useTypeWidening Should generator use parameter widening for PHP>=7.2 */ public function __construct(ReflectionFunction $reflectionFunction, string $body, bool $useTypeWidening = false) { parent::__construct(); if ($reflectionFunction->hasReturnType()) { $this->returnType = TypeGenerator::fromTypeString((string) $reflectionFunction->getReturnType()); } if ($reflectionFunction->getDocComment()) { $reflectionDocBlock = new DocBlockReflection($reflectionFunction->getDocComment()); $this->docBlock = DocBlockGenerator::fromReflection($reflectionDocBlock); } $this->returnsReference = $reflectionFunction->returnsReference(); $this->name = $reflectionFunction->getShortName(); $parameterList = new FunctionParameterList($reflectionFunction, $useTypeWidening); $this->parameters = $parameterList->getGeneratedParameters(); $this->body = $body; } /** * @return string */ public function generate() { $output = ''; $indent = $this->getIndentation(); if ($this->docBlock !== null) { $this->docBlock->setIndentation($indent); $output .= $this->docBlock->generate(); } $output .= 'function ' . ($this->returnsReference ? '& ' : '') . $this->name . '('; if (!empty($this->parameters)) { $parameterOutput = []; foreach ($this->parameters as $parameter) { $parameterOutput[] = $parameter->generate(); } $output .= implode(', ', $parameterOutput); } $output .= ')'; if ($this->returnType) { $output .= ' : ' . $this->returnType->generate(); } $output .= self::LINE_FEED . '{' . self::LINE_FEED; if ($this->body) { $output .= preg_replace('#^((?![a-zA-Z0-9_-]+;).+?)$#m', $indent . '$1', trim($this->body)) . self::LINE_FEED; } $output .= '}' . self::LINE_FEED; return $output; } } src/Proxy/Part/InterceptedMethodGenerator.php000064400000004367144760133060015367 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use ReflectionMethod; use Zend\Code\Generator\DocBlockGenerator; use Zend\Code\Generator\MethodGenerator; use Zend\Code\Reflection\DocBlockReflection; /** * Prepares the definition of intercepted method */ final class InterceptedMethodGenerator extends MethodGenerator { /** * InterceptedMethod constructor. * * @param ReflectionMethod $reflectionMethod Instance of original method * @param string $body Method body * @param bool $useTypeWidening Should generator use parameter widening for PHP>=7.2 */ public function __construct(ReflectionMethod $reflectionMethod, string $body, bool $useTypeWidening = false) { parent::__construct($reflectionMethod->getName()); $declaringClass = $reflectionMethod->getDeclaringClass(); if ($reflectionMethod->hasReturnType()) { $this->setReturnType((string) $reflectionMethod->getReturnType()); } if ($reflectionMethod->getDocComment()) { $reflectionDocBlock = new DocBlockReflection($reflectionMethod->getDocComment()); $this->setDocBlock(DocBlockGenerator::fromReflection($reflectionDocBlock)); } $this->setFinal($reflectionMethod->isFinal()); if ($reflectionMethod->isPrivate()) { $this->setVisibility(self::VISIBILITY_PRIVATE); } elseif ($reflectionMethod->isProtected()) { $this->setVisibility(self::VISIBILITY_PROTECTED); } else { $this->setVisibility(self::VISIBILITY_PUBLIC); } $this->setInterface($declaringClass->isInterface()); $this->setStatic($reflectionMethod->isStatic()); $this->setReturnsReference($reflectionMethod->returnsReference()); $this->setName($reflectionMethod->getName()); $parameterList = new FunctionParameterList($reflectionMethod, $useTypeWidening); $this->setParameters($parameterList->getGeneratedParameters()); $this->setBody($body); } } src/Proxy/Part/JoinPointPropertyGenerator.php000064400000002237144760133060015430 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use Zend\Code\Generator\PropertyGenerator; use Zend\Code\Generator\PropertyValueGenerator; /** * Prepares the definition for joinpoints private property in the class */ final class JoinPointPropertyGenerator extends PropertyGenerator { /** * Default property name for storing join points in the class */ public const NAME = '__joinPoints'; /** * JoinPointProperty constructor. * * @param array $advices List of advices to apply per class * * @throws \Zend\Code\Generator\Exception\InvalidArgumentException */ public function __construct(array $advices) { $value = new PropertyValueGenerator($advices, PropertyValueGenerator::TYPE_ARRAY_SHORT); parent::__construct(self::NAME, $value, PropertyGenerator::FLAG_PRIVATE | PropertyGenerator::FLAG_STATIC); $this->setDocBlock('List of applied advices per class'); } } src/Proxy/Part/PropertyInterceptionTrait.php000064400000004400144760133060015311 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use Go\Aop\Framework\ClassFieldAccess; use function array_key_exists; use function get_parent_class; use function method_exists; /** * Trait that holds all general logic for working with intercepted properties */ trait PropertyInterceptionTrait { /** * Holds a collection of current values for intercepted properties * * @var array */ private $__properties = []; /** * @inheritDoc */ public function &__get($name) { if (array_key_exists($name, $this->__properties)) { /** @var ClassFieldAccess $fieldAccess */ $fieldAccess = self::$__joinPoints["prop:$name"]; $fieldAccess->ensureScopeRule(); $value = &$fieldAccess->__invoke($this, ClassFieldAccess::READ, $this->__properties[$name]); } elseif (method_exists(get_parent_class(), __FUNCTION__)) { $value = parent::__get($name); } else { trigger_error("Trying to access undeclared property {$name}"); $value = null; } return $value; } /** * @inheritDoc */ public function __set($name, $value) { if (array_key_exists($name, $this->__properties)) { /** @var ClassFieldAccess $fieldAccess */ $fieldAccess = self::$__joinPoints["prop:$name"]; $fieldAccess->ensureScopeRule(); $this->__properties[$name] = $fieldAccess->__invoke( $this, ClassFieldAccess::WRITE, $this->__properties[$name], $value ); } elseif (method_exists(get_parent_class(), __FUNCTION__)) { parent::__set($name, $value); } else { $this->$name = $value; } } /** * @inheritDoc */ public function __isset($name) { return isset($this->__properties[$name]); } /** * @inheritDoc */ public function __unset($name) { $this->__properties[$name] = null; } } src/Proxy/TraitProxyGenerator.php000064400000011534144760133060013171 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy; use Go\Aop\Intercept\MethodInvocation; use Go\Core\AspectContainer; use Go\Core\AspectKernel; use Go\Proxy\Part\FunctionCallArgumentListGenerator; use ReflectionClass; use ReflectionMethod; use Zend\Code\Generator\DocBlockGenerator; use Zend\Code\Generator\TraitGenerator; use Zend\Code\Generator\ValueGenerator; use Zend\Code\Reflection\DocBlockReflection; /** * Trait proxy builder that is used to generate a trait from the list of joinpoints */ class TraitProxyGenerator extends ClassProxyGenerator { /** * Generates an child code by original class reflection and joinpoints for it * * @param ReflectionClass $originalTrait Original class reflection * @param string $parentTraitName Parent trait name to use * @param string[][] $traitAdvices List of advices for class * @param bool $useParameterWidening Enables usage of parameter widening feature */ public function __construct( ReflectionClass $originalTrait, string $parentTraitName, array $traitAdvices, bool $useParameterWidening ) { $this->advices = $traitAdvices; $dynamicMethodAdvices = $traitAdvices[AspectContainer::METHOD_PREFIX] ?? []; $staticMethodAdvices = $traitAdvices[AspectContainer::STATIC_METHOD_PREFIX] ?? []; $interceptedMethods = array_keys($dynamicMethodAdvices + $staticMethodAdvices); $generatedMethods = $this->interceptMethods($originalTrait, $interceptedMethods); $this->generator = new TraitGenerator( $originalTrait->getShortName(), $originalTrait->getNamespaceName(), null, null, [], [], $generatedMethods, DocBlockGenerator::fromReflection(new DocBlockReflection($originalTrait->getDocComment())) ); // Normalize FQDN $namespaceParts = explode('\\', $parentTraitName); $parentNormalizedName = end($namespaceParts); $this->generator->addTrait($parentNormalizedName); foreach ($interceptedMethods as $methodName) { $fullName = $parentNormalizedName . '::' . $methodName; $this->generator->addTraitAlias($fullName, $methodName . 'âž©', ReflectionMethod::IS_PROTECTED); } } /** * Returns a method invocation for the specific trait method * * @param array $advices List of advices for this trait method */ public static function getJoinPoint( string $className, string $joinPointType, string $methodName, array $advices ): MethodInvocation { static $accessor; if ($accessor === null) { $aspectKernel = AspectKernel::getInstance(); $accessor = $aspectKernel->getContainer()->get('aspect.advisor.accessor'); } $filledAdvices = []; foreach ($advices as $advisorName) { $filledAdvices[] = $accessor->$advisorName; } $joinPoint = new self::$invocationClassMap[$joinPointType]($className, $methodName . 'âž©', $filledAdvices); return $joinPoint; } /** * Creates string definition for trait method body by method reflection */ protected function getJoinpointInvocationBody(ReflectionMethod $method): string { $isStatic = $method->isStatic(); $class = '\\' . __CLASS__; $scope = $isStatic ? 'static::class' : '$this'; $prefix = $isStatic ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX; $argumentList = new FunctionCallArgumentListGenerator($method); $argumentCode = $argumentList->generate(); $argumentCode = $scope . ($argumentCode ? ", $argumentCode" : ''); $return = 'return '; if ($method->hasReturnType()) { $returnType = (string) $method->getReturnType(); if ($returnType === 'void') { // void return types should not return anything $return = ''; } } $advicesArray = new ValueGenerator($this->advices[$prefix][$method->name], ValueGenerator::TYPE_ARRAY_SHORT); $advicesArray->setArrayDepth(1); $advicesCode = $advicesArray->generate(); return <<name}', {$advicesCode}); } {$return}\$__joinPoint->__invoke($argumentCode); BODY; } /** * {@inheritDoc} */ public function generate(): string { return $this->generator->generate(); } } tests/Fixtures/project/bin/console000075500000001036144760133060013331 0ustar00#!/usr/bin/env php addCommands([ new CacheWarmupCommand(), new DebugAdvisorCommand(), new DebugAspectCommand(), new DebugWeavingCommand(), ]); $application->run(); tests/Fixtures/project/src/Annotation/Loggable.php000064400000000171144760133060016316 0ustar00getFileName(); } } tests/Fixtures/project/src/Application/ParentWithFinalMethod.php000064400000000354144760133060021136 0ustar00doSomething(*)) || execution(public Go\Tests\TestProject\Application\*->doSomethingElse(*))") */ public function afterDoSomething() { echo 'It does something else after something is done.'; } } tests/Fixtures/project/src/Aspect/InconsistentlyWeavingAspect.php000064400000002055144760133060021420 0ustar00badlyWeaved(*))") */ public function weaveBadly() { echo 'I weave badly.'; } } tests/Fixtures/project/src/Aspect/InitializationAspect.php000064400000001331144760133060020035 0ustar00logger = $logger; } /** * Writes a log info before method execution * * @param MethodInvocation $invocation * * @Pointcut\Before("@execution(Go\Tests\TestProject\Annotation\Loggable)") */ public function beforeMethod(MethodInvocation $invocation) { $this->logger->info($invocation, $invocation->getArguments()); } } tests/Fixtures/project/src/Aspect/PropertyInterceptAspect.php000064400000000713144760133060020553 0ustar00*Property)") */ public function interceptClassProperty(FieldAccess $access) { echo 'Class property intercepted!'; } } tests/Fixtures/project/src/Aspect/WeavingAspect.php000064400000001313144760133060016446 0ustar00*(*))") */ public function afterPublicMethodInTheFinalClass() { echo 'It intercepts methods in the final class'; } /** * Doesn't intercept interfaces * * @Pointcut\After("execution(public Go\Tests\TestProject\Application\FooInterface->*(*))") */ public function afterPublicMethodInTheInterface() { echo 'It does not intercept methods in the interface'; } } tests/Fixtures/project/src/Kernel/DefaultAspectKernel.php000064400000001752144760133060017603 0ustar00registerAspect(new LoggingAspect(new NullLogger())); $container->registerAspect(new DoSomethingAspect()); $container->registerAspect(new PropertyInterceptAspect()); $container->registerAspect(new Issue293Aspect()); $container->registerAspect(new InitializationAspect()); $container->registerAspect(new WeavingAspect()); } } tests/Fixtures/project/src/Kernel/InconsistentlyWeavingAspectKernel.php000064400000001444144760133060022563 0ustar00registerAspect(new LoggingAspect(new NullLogger())); $container->registerAspect(new InconsistentlyWeavingAspect(new InconsistentlyWeavedClass())); } } tests/Fixtures/project/var/cache/.gitkeep000064400000000000144760133060014443 0ustar00tests/Fixtures/project/web/configuration.php000064400000001563144760133060015333 0ustar00 [ 'kernel' => \Go\Tests\TestProject\Kernel\DefaultAspectKernel::class, 'console' => __DIR__ . '/../bin/console', 'frontController' => __DIR__ . '/../web/index.php', 'appDir' => __DIR__ . '/../', 'debug' => true, 'cacheDir' => __DIR__ . '/../var/cache/aspect', 'includePaths' => [ __DIR__ . '/../src/' ], ], 'inconsistent_weaving' => [ 'kernel' => \Go\Tests\TestProject\Kernel\InconsistentlyWeavingAspectKernel::class, 'console' => __DIR__ . '/../bin/console', 'frontController' => __DIR__ . '/../web/index.php', 'appDir' => __DIR__ . '/../', 'debug' => true, 'cacheDir' => __DIR__ . '/../var/cache/aspect', 'includePaths' => [ __DIR__ . '/../src/' ], ], ]; tests/Fixtures/project/web/index.php000064400000000551144760133060013567 0ustar00init($settings[$configuration]); tests/Go/Aop/Bridge/Doctrine/MetadataLoadInterceptorTest.php000064400000005561144760133060020060 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Bridge\Doctrine; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; use Doctrine\ORM\Mapping\ClassMetadata; use Go\Bridge\Doctrine\MetadataLoadInterceptor; use Go\Core\AspectContainer; use PHPUnit\Framework\TestCase; class MetadataLoadInterceptorTest extends TestCase { public function testItWillNotModifyClassMetadataForNonProxiedClasses(): void { $metadatas = [ new ClassMetadata('\\Some\\Class\\Name'), new ClassMetadata(sprintf('%s\\Some\\Class\\Name', AspectContainer::AOP_PROXIED_SUFFIX)), new ClassMetadata(AspectContainer::AOP_PROXIED_SUFFIX), ]; $metadataInterceptor = new MetadataLoadInterceptor(); $entityManager = $this->getMockBuilder(EntityManager::class)->disableOriginalConstructor()->getMock(); /** * @var ClassMetadata $metadata */ foreach ($metadatas as $metadata) { $metadata->isMappedSuperclass = false; $metadataInterceptor->loadClassMetadata(new LoadClassMetadataEventArgs($metadata, $entityManager)); $this->assertFalse($metadata->isMappedSuperclass); } } public function testItWillModifyClassMetadataForNonProxiedClasses(): void { $metadata = new ClassMetadata(Entity__AopProxied::class); $metadataInterceptor = new MetadataLoadInterceptor(); $entityManager = $this->getMockBuilder(EntityManager::class)->disableOriginalConstructor()->getMock(); $metadata->isMappedSuperclass = false; $metadata->isEmbeddedClass = true; $metadata->table = ['table_name']; $metadata->customRepositoryClassName = 'CustomRepositoryClass'; $metadata->fieldMappings['mappedField'] = [ 'columnName' => 'mapped_field', 'fieldName' => 'mappedField' ]; $metadata->fieldNames['mapped_field'] = 'mappedField'; $metadata->columnNames['mappedField'] = 'mapped_field'; $metadataInterceptor->loadClassMetadata(new LoadClassMetadataEventArgs($metadata, $entityManager)); $this->assertTrue($metadata->isMappedSuperclass); $this->assertFalse($metadata->isEmbeddedClass); $this->assertEquals(0, count($metadata->table)); $this->assertNull($metadata->customRepositoryClassName); $this->assertFalse(isset($metadata->fieldMappings['mappedField'])); $this->assertFalse(isset($metadata->fieldNames['mapped_field'])); $this->assertFalse(isset($metadata->columnNames['mappedField'])); } } trait SimpleTrait { private $mappedField; } class Entity__AopProxied { use SimpleTrait; } tests/Go/Aop/Framework/AbstractInterceptorTest.php000064400000003456144760133060016276 0ustar00 * * 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\Invocation; use PHPUnit\Framework\Testcase; abstract class AbstractInterceptorTest extends TestCase { /** * Concrete class name for mock, should be redefined with LSB */ const INVOCATION_CLASS = Invocation::class; /** * Returns a test advice that writes itself to the sequence * * @param array $sequenceRecorder * @return callable */ protected function getAdvice(&$sequenceRecorder) { return function () use (&$sequenceRecorder) { $sequenceRecorder[] = 'advice'; return 'advice'; }; } /** * Returns an empty invocation that can update the sequence on invocation * * @param array $sequenceRecorder * @return \PHPUnit_Framework_MockObject_MockObject|Invocation */ protected function getInvocation(&$sequenceRecorder, $throwException = false) { $invocation = $this->getMockBuilder(static::INVOCATION_CLASS)->getMock(); $invocation ->expects($this->any()) ->method('proceed') ->will( $this->returnCallback( function () use (&$sequenceRecorder, $throwException) { $sequenceRecorder[] = 'invocation'; if ($throwException) { throw new \RuntimeException('Expected exception'); } return 'invocation'; } ) ); return $invocation; } } tests/Go/Aop/Framework/AbstractJoinpointTest.php000064400000007662144760133060015754 0ustar00assertInstanceOf($expected, $advice); } } public function sortingTestSource(): array { return [ // #0 [ [ $this->createMock(AdviceAfter::class), $this->createMock(AdviceBefore::class) ], [ AdviceBefore::class, AdviceAfter::class ] ], // #1 [ [ $this->createMock(AdviceAfter::class), $this->createMock(AdviceAround::class) ], [ AdviceAfter::class, AdviceAround::class ] ], // #2 [ [ $this->createMock(AdviceBefore::class), $this->createMock(AdviceAfter::class) ], [ AdviceBefore::class, AdviceAfter::class ] ], // #3 [ [ $this->createMock(AdviceBefore::class), $this->createMock(AdviceAround::class) ], [ AdviceBefore::class, AdviceAround::class ] ], // #4 [ [ $this->createMock(AdviceAround::class), $this->createMock(AdviceAfter::class) ], [ AdviceAfter::class, AdviceAround::class ] ], // #5 [ [ $this->createMock(AdviceAround::class), $this->createMock(AdviceBefore::class) ], [ AdviceBefore::class, AdviceAround::class ] ], // #6 [ [ $this->createMock(AdviceBefore::class), $this->createMock(AdviceAround::class), $this->createMock(AdviceBefore::class), $this->createMock(AdviceAfter::class), ], [ AdviceBefore::class, AdviceBefore::class, AdviceAfter::class, AdviceAround::class, ] ], // #7 [ [ $forth = $this->getOrderedAdvice(4, 'ForthAdvice'), $first = $this->getOrderedAdvice(1, 'FirstAdvice') ], [ get_class($first), get_class($forth), ] ], ]; } /** * Returns the ordered advice * * @param int $order Order * @param string $name Mock class name * @return \PHPUnit_Framework_MockObject_MockObject|OrderedAdvice */ private function getOrderedAdvice($order, $name) { $mock = $this->createMock(OrderedAdvice::class, [], [], $name); $mock ->method('getAdviceOrder') ->willReturn($order); return $mock; } } tests/Go/Aop/Framework/AbstractMethodInvocationTest.php000064400000001527144760133060017247 0ustar00invocation = $this->getMockForAbstractClass( AbstractMethodInvocation::class, [__CLASS__, __FUNCTION__, []] ); } public function testInvocationReturnsMethod(): void { $this->assertEquals(__CLASS__, $this->invocation->getMethod()->class); $this->assertEquals('setUp', $this->invocation->getMethod()->name); } public function testProvidesAccessToAnnotations(): void { $this->assertInstanceOf(AnnotationAccess::class, $this->invocation->getMethod()); } } tests/Go/Aop/Framework/AfterInterceptorTest.php000064400000002660144760133060015570 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Framework; class AfterInterceptorTest extends AbstractInterceptorTest { public function testAdviceIsCalledAfterInvocation() { $sequence = []; $advice = $this->getAdvice($sequence); $invocation = $this->getInvocation($sequence); $interceptor = new AfterInterceptor($advice); $result = $interceptor->invoke($invocation); $this->assertEquals('invocation', $result, "Advice should not affect the return value of invocation"); $this->assertEquals(['invocation', 'advice'], $sequence, "After advice should be invoked after invocation"); } public function testAdviceIsCalledAfterExceptionInInvocation() { $sequence = []; $advice = $this->getAdvice($sequence); $invocation = $this->getInvocation($sequence, true); $interceptor = new AfterInterceptor($advice); $this->expectException('RuntimeException'); try { $interceptor->invoke($invocation); } catch (\Exception $e) { $this->assertEquals(['invocation', 'advice'], $sequence, "After advice should be invoked after invocation"); throw $e; } } } tests/Go/Aop/Framework/AfterThrowingInterceptorTest.php000064400000002647144760133060017317 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Framework; class AfterThrowingInterceptorTest extends AbstractInterceptorTest { public function testAdviceIsNotCalledAfterInvocation() { $sequence = []; $advice = $this->getAdvice($sequence); $invocation = $this->getInvocation($sequence); $interceptor = new AfterThrowingInterceptor($advice); $result = $interceptor->invoke($invocation); $this->assertEquals('invocation', $result, "Advice should not affect the return value of invocation"); $this->assertEquals(['invocation'], $sequence, "Advice should not be invoked"); } public function testAdviceIsCalledAfterExceptionInInvocation() { $sequence = []; $advice = $this->getAdvice($sequence); $invocation = $this->getInvocation($sequence, true); $interceptor = new AfterThrowingInterceptor($advice); $this->expectException('RuntimeException'); try { $interceptor->invoke($invocation); } catch (\Exception $e) { $this->assertEquals(['invocation', 'advice'], $sequence, "Advice should be invoked after invocation"); throw $e; } }} tests/Go/Aop/Framework/AroundInterceptorTest.php000064400000003134144760133060015754 0ustar00 * * 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\Invocation; class AroundInterceptorTest extends AbstractInterceptorTest { public function testInvocationIsNotCalledWithoutProceed() { $sequence = []; $advice = $this->getAdvice($sequence); // advice will not call Invocation->proceed() $invocation = $this->getInvocation($sequence); $interceptor = new AroundInterceptor($advice); $result = $interceptor->invoke($invocation); $this->assertEquals('advice', $result, "Advice should change the return value of invocation"); $this->assertEquals(['advice'], $sequence, "Only advice should be invoked"); } public function testInvocationIsCalledWithinAdvice() { $sequence = []; $advice = function (Invocation $invocation) use (&$sequence) { $sequence[] = 'advice'; $result = $invocation->proceed(); $sequence[] = 'advice'; return $result; }; $invocation = $this->getInvocation($sequence); $interceptor = new AroundInterceptor($advice); $result = $interceptor->invoke($invocation); $this->assertEquals('invocation', $result, "Advice should return an original return value"); $this->assertEquals(['advice', 'invocation', 'advice'], $sequence, "Around logic should work"); } } tests/Go/Aop/Framework/BaseInterceptorTest.php000064400000004153144760133060015400 0ustar00 * * 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\Invocation; use Go\Stubs\AbstractInterceptorMock; class BaseInterceptorTest extends AbstractInterceptorTest { /** * Concrete class name for mock, should be redefined with LSB */ const INVOCATION_CLASS = Invocation::class; public function testReturnsRawAdvice() { $sequence = []; $advice = $this->getAdvice($sequence); $interceptor = $this->getMockForAbstractClass( AbstractInterceptor::class, [$advice] ); $this->assertEquals($advice, $interceptor->getRawAdvice()); } public function testCanSerializeInterceptor() { $sequence = []; $advice = $this->getAdvice($sequence); $mock = new AbstractInterceptorMock($advice); $mockClass = get_class($mock); $mockNameLength = strlen($mockClass); $result = serialize($mock); $expected = 'C:' . $mockNameLength . ':"' . $mockClass . '":161:{a:1:{s:12:"adviceMethod";a:3:{s:5:"scope";s:6:"aspect";s:6:"method";s:26:"Go\Aop\Framework\{closure}";s:6:"aspect";s:36:"Go\Aop\Framework\BaseInterceptorTest";}}}'; $this->assertEquals($expected, $result); } public function testCanUnserializeInterceptor() { $advice = $this->getAdvice($sequence); $mock = new AbstractInterceptorMock($advice); $mockClass = get_class($mock); $mockNameLength = strlen($mockClass); // Trick to mock unserialization of advice $serialized = 'C:' . $mockNameLength .':"' . $mockClass . '":161:{a:1:{s:12:"adviceMethod";a:3:{s:5:"scope";s:6:"aspect";s:6:"method";s:26:"Go\Aop\Framework\{closure}";s:6:"aspect";s:36:"Go\Aop\Framework\BaseInterceptorTest";}}}'; $result = unserialize($serialized); $this->assertEquals($advice, $result->getRawAdvice()); } } tests/Go/Aop/Framework/BeforeInterceptorTest.php000064400000001567144760133060015736 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Framework; class BeforeInterceptorTest extends AbstractInterceptorTest { public function testAdviceIsCalledBeforeInvocation() { $sequence = []; $advice = $this->getAdvice($sequence); $invocation = $this->getInvocation($sequence); $interceptor = new BeforeInterceptor($advice); $result = $interceptor->invoke($invocation); $this->assertEquals('invocation', $result, "Advice should not affect the return value of invocation"); $this->assertEquals(['advice', 'invocation'], $sequence, "Before advice should be invoked before invocation"); } } tests/Go/Aop/Framework/ClassFieldAccessTest.php000064400000001374144760133060015444 0ustar00classField = new ClassFieldAccess(__CLASS__, 'classField', []); } public function testClassFiledReturnsProperty(): void { $this->assertEquals(__CLASS__, $this->classField->getField()->class); $this->assertEquals('classField', $this->classField->getField()->name); } public function testProvidesAccessToAnnotations(): void { $this->assertInstanceOf(AnnotationAccess::class, $this->classField->getField()); } } tests/Go/Aop/Framework/DynamicClosureSplatMethodInvocationTest.php000064400000007153144760133060021432 0ustar00createMock(First::class); $invocation = new DynamicClosureMethodInvocation(First::class, $methodName, []); $result = $invocation($child); $this->assertEquals($expectedResult, $result); } public function testValueChangedByReference(): void { $child = $this->createMock(First::class); $invocation = new DynamicClosureMethodInvocation(First::class, 'passByReference', []); $value = 'test'; $result = $invocation($child, [&$value]); $this->assertEquals(null, $result); $this->assertEquals(null, $value); } public function testInvocationWithDynamicArguments(): void { $child = $this->createMock(First::class); $invocation = new DynamicClosureMethodInvocation(First::class, 'variableArgsTest', []); $args = []; $expected = ''; for ($i=0; $i<10; $i++) { $args[] = $i; $expected .= $i; $result = $invocation($child, $args); $this->assertEquals($expected, $result); } } public function testInvocationWithVariadicArguments(): void { $child = $this->createMock(First::class); $invocation = new DynamicClosureMethodInvocation(First::class, 'variadicArgsTest', []); $args = []; $expected = ''; for ($i=0; $i<10; $i++) { $args[] = $i; $expected .= $i; $result = $invocation($child, $args); $this->assertEquals($expected, $result); } } public function testRecursionWorks(): void { $child = $this->createMock(First::class); $invocation = new DynamicClosureMethodInvocation(First::class, 'recursion', []); $child->expects($this->exactly(5))->method('recursion')->will($this->returnCallback( function ($value, $level) use ($child, $invocation) { return $invocation($child, [$value, $level]); } )); $this->assertEquals(5, $child->recursion(5,0)); $this->assertEquals(20, $child->recursion(5,3)); } public function testInterceptorIsCalledForInvocation(): void { $child = $this->createMock(First::class); $value = 'test'; $advice = $this->createMock(Interceptor::class); $advice->expects($this->once()) ->method('invoke') ->will($this->returnCallback(function (MethodInvocation $object) use (&$value) { $value = 'ok'; return $object->proceed(); })); $invocation = new DynamicClosureMethodInvocation(First::class, 'publicMethod', [$advice]); $result = $invocation($child, []); $this->assertEquals('ok', $value); $this->assertEquals(T_PUBLIC, $result); } public function dynamicMethodsBatch(): array { return [ ['publicMethod', T_PUBLIC], ['protectedMethod', T_PROTECTED], // array('privateMethod', T_PRIVATE), This will throw an ReflectionException, need to add use case for that ]; } } tests/Go/Aop/Framework/ReflectionConstructorInvocationTest.php000064400000007041144760133060020700 0ustar00 * * 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\Core\AspectContainer; class ReflectionConstructorInvocationTest extends AbstractInterceptorTest { public function testCanCreateObjectDuringInvocation(): void { $invocation = new ReflectionConstructorInvocation(\Exception::class, 'unused', []); $result = $invocation->__invoke(); $this->assertInstanceOf(\Exception::class, $result); } public function testKnowsAboutSpecialClassSuffix(): void { $specialName = \Exception::class . AspectContainer::AOP_PROXIED_SUFFIX; $invocation = new ReflectionConstructorInvocation($specialName, 'unused', []); $result = $invocation->__invoke(); $this->assertInstanceOf(\Exception::class, $result); } public function testCanExecuteAdvicesDuringConstruct(): void { $sequence = []; $advice = $this->getAdvice($sequence); $before = new BeforeInterceptor($advice); $invocation = new ReflectionConstructorInvocation(\Exception::class, 'unused', [$before]); $this->assertEmpty($sequence); $invocation->__invoke(['Message', 100]); $this->assertContains('advice', $sequence); } public function testStringRepresentation(): void { $invocation = new ReflectionConstructorInvocation(\Exception::class, 'unused', []); $name = (string)$invocation; $this->assertEquals('initialization(Exception)', $name); } public function testReturnsConstructor(): void { $invocation = new ReflectionConstructorInvocation(\Exception::class, 'unused', []); $ctor = $invocation->getConstructor(); $this->assertInstanceOf(\ReflectionMethod::class, $ctor); $this->assertEquals('__construct', $ctor->name); } public function testReturnsThis(): void { $invocation = new ReflectionConstructorInvocation(\Exception::class, 'unused', []); $instance = $invocation->getThis(); $this->assertNull($instance); $object = $invocation->__invoke(['Some error', 100]); $this->assertEquals($object, $invocation->getThis()); } public function testCanCreateAnInstanceEvenWithNonPublicConstructor(): void { try { $testClassInstance = new class('Test') { public $message; private function __construct(string $message) { $this->message = $message; } }; $loadedClass = get_class($testClassInstance); } catch (\Error $e) { // let's look for all class names to find our anonymous one foreach (get_declared_classes() as $loadedClass) { $refClass = new \ReflectionClass($loadedClass); if ($refClass->getFileName() === __FILE__ && strpos($refClass->getName(), 'anonymous') !== false) { // loadedClass will contain our anonymous class break; } } } $testClassName = $loadedClass; $invocation = new ReflectionConstructorInvocation($testClassName, 'unused', []); $result = $invocation->__invoke(['Hello']); $this->assertInstanceOf($testClassName, $result); $this->assertSame('Hello', $result->message); } } tests/Go/Aop/Framework/StaticClosureMethodInvocationTest.php000064400000007115144760133060020267 0ustar00assertEquals($expectedResult, $result); } /** * Tests static method invocations with Late Static Binding * * @dataProvider staticLsbMethodsBatch * @param string $methodName Method to invoke */ public function testStaticLsbIsWorking($methodName): void { $invocation = new StaticClosureMethodInvocation(First::class, $methodName, []); $result = $invocation(First::class); $this->assertEquals(First::class, $result); } public function testValueChangedByReference(): void { $invocation = new StaticClosureMethodInvocation(First::class, 'staticPassByReference', []); $value = 'test'; $result = $invocation(First::class, [&$value]); $this->assertEquals(null, $result); $this->assertEquals(null, $value); } public function testRecursionWorks(): void { $invocation = new StaticClosureMethodInvocation(First::class, 'staticLsbRecursion', []); FirstStatic::init($invocation); $this->assertEquals(5, FirstStatic::staticLsbRecursion(5,0)); $this->assertEquals(20, FirstStatic::staticLsbRecursion(5,3)); } public function testAdviceIsCalledForInvocation(): void { $value = 'test'; $advice = new BeforeInterceptor(function () use (&$value) { $value = 'ok'; }); $invocation = new StaticClosureMethodInvocation(First::class, 'staticSelfPublic', [$advice]); $result = $invocation(First::class, []); $this->assertEquals('ok', $value); $this->assertEquals(T_PUBLIC, $result); } public function testInvocationWithDynamicArguments(): void { $invocation = new StaticClosureMethodInvocation(First::class, 'staticVariableArgsTest', []); $args = []; $expected = ''; for ($i=0; $i<10; $i++) { $args[] = $i; $expected .= $i; $result = $invocation(First::class, $args); $this->assertEquals($expected, $result); } } public function testInvocationWithVariadicArguments(): void { $invocation = new StaticClosureMethodInvocation(First::class, 'staticVariadicArgsTest', []); $args = []; $expected = ''; for ($i=0; $i<10; $i++) { $args[] = $i; $expected .= $i; $result = $invocation(First::class, $args); $this->assertEquals($expected, $result); } } public function staticSelfMethodsBatch(): array { return [ ['staticSelfPublic', T_PUBLIC], ['staticSelfProtected', T_PROTECTED], ['staticSelfPublicAccessPrivate', T_PRIVATE], ]; } public function staticLsbMethodsBatch(): array { return [ ['staticLsbPublic'], ['staticLsbProtected'], ]; } } tests/Go/Aop/Pointcut/PointcutParserTest.php000064400000010371144760133060015140 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; /** * Class PointcutParserTest defines common check for valid grammar parsing */ use Dissect\Lexer\Lexer; use Doctrine\Common\Annotations\Reader; use Go\Core\AspectContainer; use PHPUnit\Framework\TestCase; class PointcutParserTest extends TestCase { /** * @var null|Lexer */ protected $lexer; /** * @var null|PointcutParser */ protected $parser; /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); $this->lexer = new PointcutLexer(); $container = $this->createMock(AspectContainer::class); $annotReader = $this->createMock(Reader::class); $this->parser = new PointcutParser(new PointcutGrammar($container, $annotReader)); } /** * Tests parsing * * @dataProvider validPointcutDefinitions */ public function testParsingExpression($pointcutExpression): void { $stream = $this->lexer->lex($pointcutExpression); $result = $this->parser->parse($stream); $this->assertNotNull($result); } public function validPointcutDefinitions(): array { return [ // Execution pointcuts ['execution(public Example->method(*))'], ['execution(public Example->method|method1|method2(*))'], ['execution(final public Example\Aspect\*->method*(*))'], ['execution(protected|public **::*someStatic*Method*(*))'], // Return-type hints for PHP7 ['execution(public Example->method(*): string)'], // This will match property that has First\Second\Annotation\Class annotation ['@access(First\Second\Annotation\Class)'], // This will match method execution that has First\Second\Annotation\Class annotation ['@execution(First\Second\Annotation\Class)'], // This will match all the methods in all classes of Go\Aspects\Blog\Package. ['within(Go\Aspects\Blog\Package\*)'], // This will match all the methods in all classes of Go\Aspects\Blog\Package and its sub packages. ['within(Go\Aspects\Blog\Package\**)'], // This will match all the methods in the DemoClass. ['within(Go\Aspects\Blog\Package\DemoClass)'], // This will match all the methods which are in classes which implement DemoInterface. ['within(DemoInterface+)'], // This will match all the methods in the class with specific annotation. ['@within(First\Second\Annotation\Class)'], // Access pointcuts ['access(public|protected Example\Aspect\*->property*)'], ['access(protected Test\Class*->someProtected*Property)'], // Logic pointcuts ['!within(DemoInterface\Test+)'], ['within(DemoInterface+) && within(Some\Namespace\**)'], ['within(DemoInterface+) || within(Some\Namespace\**)'], // Parenthesis ['within(DemoInterface+) && ( within(**) || within(*) )'], // Control flow execution pointcuts ['cflowbelow(execution(public Example->method(*)))'], // Function pointcut ['execution(Demo\*\Test\**\*(*))'], ['execution(Demo\Namespace\array_*_er(*))'], ['execution(**\*(*))'], // Function with return-type ['execution(Demo\*\Test\**\*(*): bool)'], // Dynamic pointcut for methods via __callStatic and __call ['dynamic(public Demo\Example\DynamicMethodsDemo::find*(*))'], ['dynamic(public Demo\Example\DynamicMethodsDemo->save*(*))'], // This will match static initialization pointcut ['staticinitialization(Some\Specific\Class\**)'], // This will match all methods, but not inherited ['execution(public **->*(*)) && !matchInherited()'], // This will match dynamic initialization pointcut ['initialization(Some\Specific\Class\**)'], ]; } } tests/Go/Aop/Pointcut/SignaturePointcutTest.php000064400000007612144760133060015651 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Pointcut; use Go\Aop\PointFilter; use Go\Aop\Support\NotPointFilter; use Go\Aop\Support\TruePointFilter; use Go\Stubs\First; use PHPUnit\Framework\TestCase; class SignaturePointcutTest extends TestCase { private const STUB_CLASS = First::class; /** * Tests that method matched by name directly */ public function testDirectMethodMatchByName(): void { $filterKind = PointFilter::KIND_METHOD; $pointcut = new SignaturePointcut($filterKind, 'publicMethod', TruePointFilter::getInstance()); $matched = $pointcut->matches(new \ReflectionMethod(self::STUB_CLASS, 'publicMethod')); $this->assertTrue($matched, "Pointcut should match this method"); } /** * Tests that pointcut can match property */ public function testCanMatchProperty(): void { $filterKind = PointFilter::KIND_METHOD; $pointcut = new SignaturePointcut($filterKind, 'public', TruePointFilter::getInstance()); $matched = $pointcut->matches(new \ReflectionProperty(self::STUB_CLASS, 'public')); $this->assertTrue($matched, "Pointcut should match this property"); } /** * Tests that pointcut won't match if modifier filter is not match */ public function testWontMatchModifier(): void { $trueInstance = TruePointFilter::getInstance(); $notInstance = new NotPointFilter($trueInstance); $filterKind = PointFilter::KIND_METHOD; $pointcut = new SignaturePointcut($filterKind, 'publicMethod', $notInstance); $matched = $pointcut->matches(new \ReflectionMethod(self::STUB_CLASS, 'publicMethod')); $this->assertFalse($matched, "Pointcut should not match modifier"); } /** * Tests that pattern is working correctly */ public function testRegularPattern(): void { $filterKind = PointFilter::KIND_METHOD; $pointcut = new SignaturePointcut($filterKind, '*Method', TruePointFilter::getInstance()); $matched = $pointcut->matches(new \ReflectionMethod(self::STUB_CLASS, 'publicMethod')); $this->assertTrue($matched, "Pointcut should match this method"); $matched = $pointcut->matches(new \ReflectionMethod(self::STUB_CLASS, 'protectedMethod')); $this->assertTrue($matched, "Pointcut should match this method"); } /** * Tests that multiple pattern is matching */ public function testMultipleRegularPattern(): void { $filterKind = PointFilter::KIND_METHOD; $pointcut = new SignaturePointcut($filterKind, 'publicMethod|protectedMethod', TruePointFilter::getInstance()); $matched = $pointcut->matches(new \ReflectionMethod(self::STUB_CLASS, 'publicMethod')); $this->assertTrue($matched, "Pointcut should match this method"); $matched = $pointcut->matches(new \ReflectionMethod(self::STUB_CLASS, 'protectedMethod')); $this->assertTrue($matched, "Pointcut should match this method"); } /** * Tests that multiple pattern is using strict matching * * @link https://github.com/lisachenko/go-aop-php/issues/115 */ public function testIssue115(): void { $filterKind = PointFilter::KIND_METHOD; $pointcut = new SignaturePointcut($filterKind, 'public|Public', TruePointFilter::getInstance()); $matched = $pointcut->matches(new \ReflectionMethod(self::STUB_CLASS, 'publicMethod')); $this->assertFalse($matched, "Pointcut should match strict"); $matched = $pointcut->matches(new \ReflectionMethod(self::STUB_CLASS, 'staticLsbPublic')); $this->assertFalse($matched, "Pointcut should match strict"); } } tests/Go/Aop/Support/AndPointFilterTest.php000064400000003217144760133060014710 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; use PHPUnit\Framework\TestCase; class AndPointFilterTest extends TestCase { /** * Tests that filter intersect different kinds of filters */ public function testKindIsIntersected(): void { $first = $this->createMock(PointFilter::class); $first ->method('getKind') ->willReturn(PointFilter::KIND_METHOD | PointFilter::KIND_PROPERTY); $second = $this->createMock(PointFilter::class); $second ->method('getKind') ->willReturn(PointFilter::KIND_METHOD | PointFilter::KIND_FUNCTION); $filter = new AndPointFilter($first, $second); $this->assertEquals(PointFilter::KIND_METHOD, $filter->getKind()); } /** * @dataProvider logicCases */ public function testMatches(PointFilter $first, PointFilter $second, $expected): void { $filter = new AndPointFilter($first, $second); $result = $filter->matches(new \ReflectionClass(__CLASS__) /* anything */); $this->assertSame($expected, $result); } public function logicCases(): array { $true = TruePointFilter::getInstance(); $false = new NotPointFilter($true); return [ [$false, $false, false], [$false, $true, false], [$true, $false, false], [$true, $true, true] ]; } } tests/Go/Aop/Support/NotPointFilterTest.php000064400000001627144760133060014751 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; use PHPUnit\Framework\TestCase; class NotPointFilterTest extends TestCase { /** * @dataProvider logicCases */ public function testMatches(PointFilter $first, $expected): void { $filter = new NotPointFilter($first); $result = $filter->matches(new \ReflectionClass(__CLASS__) /* anything */); $this->assertSame($expected, $result); } public function logicCases(): array { $true = TruePointFilter::getInstance(); $false = new NotPointFilter($true); return [ [$false, true], [$true, false] ]; } } tests/Go/Aop/Support/OrPointFilterTest.php000064400000003340144760133060014563 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use Go\Aop\PointFilter; use PHPUnit\Framework\TestCase; class OrPointFilterTest extends TestCase { /** * Tests that filter combined different kinds of filters */ public function testKindIsCombined(): void { $first = $this->createMock(PointFilter::class); $first ->method('getKind') ->willReturn(PointFilter::KIND_METHOD | PointFilter::KIND_PROPERTY); $second = $this->createMock(PointFilter::class); $second ->method('getKind') ->willReturn(PointFilter::KIND_METHOD | PointFilter::KIND_FUNCTION); $filter = new OrPointFilter($first, $second); $expected = PointFilter::KIND_METHOD | PointFilter::KIND_FUNCTION | PointFilter::KIND_PROPERTY; $this->assertEquals($expected, $filter->getKind()); } /** * @dataProvider logicCases */ public function testMatches(PointFilter $first, PointFilter $second, $expected): void { $filter = new OrPointFilter($first, $second); $result = $filter->matches(new \ReflectionClass(__CLASS__) /* anything */); $this->assertSame($expected, $result); } public function logicCases(): array { $true = TruePointFilter::getInstance(); $false = new NotPointFilter($true); return [ [$false, $false, false], [$false, $true, true], [$true, $false, true], [$true, $true, true] ]; } } tests/Go/Aop/Support/TruePointFilterTest.php000064400000002011144760133060015114 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Aop\Support; use PHPUnit\Framework\TestCase; /** * Generated by PHPUnit_SkeletonGenerator 1.2.0 on 2012-11-28 at 15:42:10. */ class TruePointFilterTest extends TestCase { /** * @var TruePointFilter */ protected $filter; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->filter = TruePointFilter::getInstance(); } /** * Test that true matcher always matches the class */ public function testMatches(): void { // Works correctly with ReflectionClass $class = new \ReflectionClass('stdClass'); $this->assertTrue($this->filter->matches($class)); } } tests/Go/Console/Command/CacheWarmupCommandTest.php000064400000000777144760133060016321 0ustar00loadConfiguration(); $this->clearCache(); } public function testItWarmsUpCache() { $this->assertFileNotExists($this->configuration['cacheDir']); $this->warmUp(); $this->assertClassIsWoven(Main::class); } } tests/Go/Console/Command/DebugAdvisorCommandTest.php000064400000002424144760133060016467 0ustar00execute('debug:advisor'); $expected = [ 'List of registered advisors in the container', 'Go\Tests\TestProject\Aspect\LoggingAspect->beforeMethod', '@execution(Go\Tests\TestProject\Annotation\Loggable)', ]; foreach ($expected as $string) { $this->assertContains($string, $output); } } public function testItDisplaysStatedAdvisorDebugInfo() { $output = self::execute('debug:advisor', '--advisor="Go\Tests\TestProject\Aspect\LoggingAspect->beforeMethod"'); $enumerator = new Enumerator(realpath($this->configuration['appDir'].'/src')); $srcFilesCount = iterator_count($enumerator->enumerate()); $expected = [ sprintf('Total %s files to analyze.', $srcFilesCount), '-> matching method Go\Tests\TestProject\Application\Main->doSomething', ]; foreach ($expected as $string) { $this->assertContains($string, $output); } } } tests/Go/Console/Command/DebugAspectCommandTest.php000064400000001176144760133060016302 0ustar00execute('debug:aspect'); $expected = [ 'Go\Tests\TestProject\Kernel\DefaultAspectKernel has following enabled aspects', 'Go\Tests\TestProject\Aspect\LoggingAspect', 'Go\Tests\TestProject\Aspect\LoggingAspect->beforeMethod' ]; foreach ($expected as $string) { $this->assertContains($string, $output); } } } tests/Go/Console/Command/DebugWeavingCommandTest.php000064400000001327144760133060016461 0ustar00execute('debug:weaving', null, false, 1)); $this->assertRegexp('/.+InconsistentlyWeavedClass.php.+generated.+on.+second.+"warmup".+pass.+/', $output); $this->assertRegexp('/.+Main.php".+is.+consistently.+weaved.+/', $output); $this->assertContains('[ERROR] Weaving is unstable, there are 1 reported error(s).', $output); } protected function getConfigurationName(): string { return 'inconsistent_weaving'; } } tests/Go/Core/AdviceMatcherTest.php000064400000007557144760133060013235 0ustar00adviceMatcher = new AdviceMatcher(); $reflectionFile = new ReflectionFile(__FILE__); $this->reflectionClass = $reflectionFile->getFileNamespace(__NAMESPACE__)->getClass(__CLASS__); } /** * Verifies that empty result will be returned without aspects and advisors */ public function testGetEmptyAdvicesForClass() { // by reflection $advices = $this->adviceMatcher->getAdvicesForClass($this->reflectionClass, []); $this->assertEmpty($advices); } /** * Check that list of advices for method works correctly */ public function testGetSingleMethodAdviceForClassFromAdvisor() { $funcName = __FUNCTION__; $pointcut = $this->createMock(Pointcut::class); $pointcut ->expects($this->any()) ->method('getClassFilter') ->will($this->returnValue(TruePointFilter::getInstance())); $pointcut ->expects($this->any()) ->method('matches') ->will($this->returnCallback(function ($point) use ($funcName) { return $point->name === $funcName; })); $pointcut ->expects($this->any()) ->method('getKind') ->will($this->returnValue(Pointcut::KIND_METHOD)); $advice = $this->createMock(Advice::class); $advisor = new DefaultPointcutAdvisor($pointcut, $advice); $advices = $this->adviceMatcher->getAdvicesForClass($this->reflectionClass, ['advisor' => $advisor]); $this->assertArrayHasKey(AspectContainer::METHOD_PREFIX, $advices); $this->assertArrayHasKey($funcName, $advices[AspectContainer::METHOD_PREFIX]); $this->assertCount(1, $advices[AspectContainer::METHOD_PREFIX]); } /** * Check that list of advices for fields works correctly */ public function testGetSinglePropertyAdviceForClassFromAdvisor() { $propName = 'adviceMatcher'; // $this->adviceMatcher; $pointcut = $this->createMock(Pointcut::class); $pointcut ->expects($this->any()) ->method('getClassFilter') ->will($this->returnValue(TruePointFilter::getInstance())); $pointcut ->expects($this->any()) ->method('matches') ->will($this->returnCallback(function ($point) use ($propName) { return $point->name === $propName; })); $pointcut ->expects($this->any()) ->method('getKind') ->will($this->returnValue(Pointcut::KIND_PROPERTY)); $advice = $this->createMock(Advice::class); $advisor = new DefaultPointcutAdvisor($pointcut, $advice); $advices = $this->adviceMatcher->getAdvicesForClass($this->reflectionClass, ['advisor' => $advisor]); $this->assertArrayHasKey(AspectContainer::PROPERTY_PREFIX, $advices); $this->assertArrayHasKey($propName, $advices[AspectContainer::PROPERTY_PREFIX]); $this->assertCount(1, $advices[AspectContainer::PROPERTY_PREFIX]); } } tests/Go/Core/GoAspectContainerTest.php000064400000006124144760133060014073 0ustar00container = new GoAspectContainer(); $this->container->set('kernel.options', []); $this->container->set('kernel.interceptFunctions', false); } /** * Tests that all services are registered * * @dataProvider internalServicesList */ public function testAllServicesAreConfigured($serviceId) { $service = $this->container->get($serviceId); $this->assertNotNull($service); } public function internalServicesList() { return [ ['aspect.loader'], ['aspect.advice_matcher'], ['aspect.annotation.cache'], ['aspect.annotation.reader'], ['aspect.pointcut.lexer'], ['aspect.pointcut.parser'], ]; } /** * Tests that pointcut can be registered and accessed */ public function testPointcutCanBeRegisteredAndReceived() { $pointcut = $this->createMock(Pointcut::class); $this->container->registerPointcut($pointcut, 'test'); $this->assertSame($pointcut, $this->container->getPointcut('test')); // Verify that tag is working $pointcuts = $this->container->getByTag('pointcut'); $this->assertSame(['pointcut.test' => $pointcut], $pointcuts); } /** * Tests that pointcut can be registered and accessed */ public function testAdvisorCanBeRegistered() { $advisor = $this->createMock(Advisor::class); $this->container->registerAdvisor($advisor, 'test'); // Verify that tag is working $advisors = $this->container->getByTag('advisor'); $this->assertSame(['advisor.test' => $advisor], $advisors); } /** * Tests that aspect can be registered and accessed */ public function testAspectCanBeRegisteredAndReceived() { $aspect = $this->createMock(Aspect::class); $aspectClass = get_class($aspect); $this->container->registerAspect($aspect); $this->assertSame($aspect, $this->container->getAspect($aspectClass)); // Verify that tag is working $aspects = $this->container->getByTag('aspect'); $this->assertSame(["aspect.{$aspectClass}" => $aspect], $aspects); } /** * Tests that container resources can be added and isFresh works correctly */ public function testResourceManagement() { // Without resources this should be always true $isFresh = $this->container->isFresh(time()); $this->assertTrue($isFresh); $this->container->addResource(__FILE__); $realMtime = filemtime(__FILE__); $isFresh = $this->container->isFresh($realMtime - 3600); $this->assertFalse($isFresh); $isFresh = $this->container->isFresh($realMtime + 3600); $this->assertTrue($isFresh); } } tests/Go/Functional/BaseFunctionalTest.php000064400000033326144760133060014636 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Functional; use Go\Core\AspectContainer; use Go\Instrument\PathResolver; use Go\PhpUnit\ClassIsNotWovenConstraint; use Go\PhpUnit\ClassWovenConstraint; use Go\PhpUnit\ClassAdvisorIdentifier; use Go\PhpUnit\ClassMemberWovenConstraint; use Go\PhpUnit\ClassMemberNotWovenConstraint; use PHPUnit\Framework\Testcase; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; /** * Base class for functional tests. */ abstract class BaseFunctionalTest extends TestCase { /** * Configuration which ought to be used in this test suite. * * @var array */ protected $configuration; /** * {@inheritdoc} */ public function setUp() { $this->loadConfiguration(); $this->clearCache(); $this->warmUp(); } /** * {@inheritdoc} */ public function tearDown() { $this->clearCache(); } /** * Clear Go! AOP cache. */ protected function clearCache() { $filesystem = new Filesystem(); // We need to normalize path to prevent Windows 260-length filename trouble $absoluteCacheDir = PathResolver::realpath($this->configuration['cacheDir']); if ($filesystem->exists($absoluteCacheDir)) { $filesystem->remove($absoluteCacheDir); } } /** * Warms up Go! AOP cache. */ protected function warmUp(): void { $this->execute('cache:warmup:aop'); } /** * Get configuration name. * * Get configuration name from available configurations settings defined in * /tests/Fixtures/project/web/configuration.php used for executing this * functional test suite. * * Override this method to use desired configuration settings. */ protected function getConfigurationName(): string { return 'default'; } /** * Load configuration settings. * * Load configuration settings which ought to be used in this test suite, * defined in /tests/Fixtures/project/web/configuration.php. */ protected function loadConfiguration() { if (!$this->configuration) { $configurations = require __DIR__ . '/../../Fixtures/project/web/configuration.php'; $this->configuration = $configurations[$this->getConfigurationName()]; } } /** * Execute console command. * * @param string $command Command to execute. * @param string|null $args Command arguments to append, if any. * @param bool $expectSuccess Should command be executed successfully * @param null|int $expectedExitCode If provided, exit code will be asserted. * * @return string Console output. */ protected function execute($command, $args = null, $expectSuccess = true, $expectedExitCode = null): string { $phpExecutable = (new PhpExecutableFinder())->find(); $commandStatement = sprintf('%s %s --no-ansi %s %s %s', $phpExecutable, $this->configuration['console'], $command, $this->configuration['frontController'], $args ?? '' ); $process = new Process($commandStatement, null, ['GO_AOP_CONFIGURATION' => $this->getConfigurationName()]); $process->inheritEnvironmentVariables(); $process->run(); if ($expectSuccess) { $this->assertTrue($process->isSuccessful(), sprintf('Unable to execute "%s" command, got output: "%s".', $command, $process->getOutput())); } else { $this->assertFalse($process->isSuccessful(), sprintf('Command "%s" excuted successfully, even if it is expected to fail, got output: "%s".', $command, $process->getOutput())); } if (null !== $expectedExitCode) { $this->assertEquals($expectedExitCode, $process->getExitCode(), 'Assert that exit code is matched.'); } return $process->getOutput(); } /** * Assert that class is woven. * * @param string $class Full qualified class name which is subject of weaving. * @param string $message Assertion info message. */ protected function assertClassIsWoven($class, $message = '') { $constraint = new ClassWovenConstraint($this->configuration); self::assertThat($class, $constraint, $message); } /** * Assert that class is not woven. * * @param string $class Full qualified class name which should not be subject of weaving. * @param string $message Assertion info message. */ protected function assertClassIsNotWoven($class, $message = '') { $constraint = new ClassIsNotWovenConstraint($this->configuration); self::assertThat($class, $constraint, $message); } /** * Assert that class static method is woven. * * @param string $class Full qualified class name which is subject of weaving. * @param string $staticMethodName Name of static method. * @param null|string $advisorIdentifier Expected advisor identifier to be registered within proxy class, or NULL if any. * @param null|int $index Index of advisor identifier, or null if order is not important. * @param string $message Assertion info message. */ protected function assertStaticMethodWoven($class, $staticMethodName, $advisorIdentifier = null, $index = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, $staticMethodName, AspectContainer::STATIC_METHOD_PREFIX, $advisorIdentifier, $index ); $constraint = new ClassMemberWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class static method is not woven. * * @param string $class Full qualified class name which is subject of weaving. * @param string $staticMethodName Name of static method. * @param null|string $advisorIdentifier Expected advisor identifier not to be registered within proxy class, or NULL none should be registered. * @param string $message Assertion info message. */ protected function assertStaticMethodNotWoven($class, $staticMethodName, $advisorIdentifier = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, $staticMethodName, AspectContainer::STATIC_METHOD_PREFIX, $advisorIdentifier ); $constraint = new ClassMemberNotWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class method is woven. * * @param string $class Full qualified class name which is subject of weaving. * @param string $methodName Name of method. * @param null|string $advisorIdentifier Expected advisor identifier to be registered within proxy class, or NULL if any. * @param null|int $index Index of advisor identifier, or null if order is not important. * @param string $message Assertion info message. */ protected function assertMethodWoven($class, $methodName, $advisorIdentifier = null, $index = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, $methodName, AspectContainer::METHOD_PREFIX, $advisorIdentifier, $index ); $constraint = new ClassMemberWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class method is not woven. * * @param string $class Full qualified class name which is subject of weaving. * @param string $methodName Name of method. * @param null|string $advisorIdentifier Expected advisor identifier not to be registered within proxy class, or NULL if none should be registered. * @param string $message Assertion info message. */ protected function assertMethodNotWoven($class, $methodName, $advisorIdentifier = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, $methodName, AspectContainer::METHOD_PREFIX, $advisorIdentifier ); $constraint = new ClassMemberNotWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class property is woven. * * @param string $class Full qualified class name which is subject of weaving. * @param string $propertyName Property name. * @param null|string $advisorIdentifier Expected advisor identifier to be registered within proxy class, or NULL if any. * @param null|int $index Index of advisor identifier, or null if order is not important. * @param string $message Assertion info message. */ protected function assertPropertyWoven($class, $propertyName, $advisorIdentifier = null, $index = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, $propertyName, AspectContainer::PROPERTY_PREFIX, $advisorIdentifier, $index ); $constraint = new ClassMemberWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class property is not woven. * * @param string $class Full qualified class name which is subject of weaving. * @param string $propertyName Property name. * @param null|string $advisorIdentifier Expected advisor identifier not to be registered within proxy class, or NULL if none should be registered. * @param string $message Assertion info message. */ protected function assertPropertyNotWoven($class, $propertyName, $advisorIdentifier = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, $propertyName, $advisorIdentifier, AspectContainer::PROPERTY_PREFIX ); $constraint = new ClassMemberNotWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class initialization is woven. * * @param string $class Full qualified class name which is subject of weaving. * @param null|string $advisorIdentifier Expected advisor identifier to be registered within proxy class, or NULL if any. * @param null|int $index Index of advisor identifier, or null if order is not important. * @param string $message Assertion info message. */ protected function assertClassInitializationWoven($class, $advisorIdentifier = null, $index = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, 'root', AspectContainer::INIT_PREFIX, $advisorIdentifier, $index ); $constraint = new ClassMemberWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class initialization is not woven. * * @param string $class Full qualified class name which is subject of weaving. * @param null|string $advisorIdentifier Expected advisor identifier not to be registered within proxy class, or NULL if none should be registered. * @param string $message Assertion info message. */ protected function assertClassInitializationNotWoven($class, $advisorIdentifier = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, 'root', AspectContainer::INIT_PREFIX, $advisorIdentifier ); $constraint = new ClassMemberNotWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class static initialization is woven. * * @param string $class Full qualified class name which is subject of weaving. * @param null|string $advisorIdentifier Expected advisor identifier to be registered within proxy class, or NULL if any. * @param null|int $index Index of advisor identifier, or null if order is not important. * @param string $message Assertion info message. */ protected function assertClassStaticInitializationWoven($class, $advisorIdentifier = null, $index = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, 'root', AspectContainer::STATIC_INIT_PREFIX, $advisorIdentifier, $index ); $constraint = new ClassMemberWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } /** * Assert that class static initialization is not woven. * * @param string $class Full qualified class name which is subject of weaving. * @param null|string $advisorIdentifier Expected advisor identifier not to be registered within proxy class, or NULL if none should be registered. * @param string $message Assertion info message. */ protected function assertClassStaticInitializationNotWoven($class, $advisorIdentifier = null, $message = '') { $identifier = new ClassAdvisorIdentifier( $class, 'root', AspectContainer::STATIC_INIT_PREFIX, $advisorIdentifier ); $constraint = new ClassMemberNotWovenConstraint($this->configuration); self::assertThat($identifier, $constraint, $message); } } tests/Go/Functional/ClassWeavingTest.php000064400000006044144760133060014324 0ustar00assertPropertyWoven(Main::class, 'publicClassProperty', 'advisor.Go\\Tests\\TestProject\\Aspect\\PropertyInterceptAspect->interceptClassProperty'); $this->assertPropertyWoven(Main::class, 'protectedClassProperty', 'advisor.Go\\Tests\\TestProject\\Aspect\\PropertyInterceptAspect->interceptClassProperty'); // it does not weaves Main class private property $this->assertPropertyNotWoven(Main::class, 'privateClassProperty', 'advisor.Go\\Tests\\TestProject\\Aspect\\PropertyInterceptAspect->interceptClassProperty'); } /** * test for https://github.com/goaop/framework/issues/335 */ public function testItDoesNotWeaveAbstractMethods() { // it weaves Main class $this->assertClassIsWoven(Main::class); // it weaves Main class methods $this->assertMethodWoven(Main::class, 'doSomething', 'advisor.Go\\Tests\\TestProject\\Aspect\\LoggingAspect->beforeMethod', 0); $this->assertMethodWoven(Main::class, 'doSomething', 'advisor.Go\\Tests\\TestProject\\Aspect\\DoSomethingAspect->afterDoSomething', 1); $this->assertMethodWoven(Main::class, 'doSomethingElse', 'advisor.Go\\Tests\\TestProject\\Aspect\\DoSomethingAspect->afterDoSomething'); // it does not weaves AbstractBar class $this->assertClassIsNotWoven(AbstractBar::class); } public function testClassInitializationWeaving() { $this->assertClassInitializationWoven(Main::class, 'advisor.Go\\Tests\\TestProject\\Aspect\\InitializationAspect->beforeInstanceInitialization'); $this->assertClassStaticInitializationWoven(Main::class, 'advisor.Go\\Tests\\TestProject\\Aspect\\InitializationAspect->afterClassStaticInitialization'); } public function testItWeavesFinalClasses() { // it weaves FinalClass class $this->assertClassIsWoven(FinalClass::class); /* @see \Go\Tests\TestProject\Application\FinalClass::somePublicMethod */ // it weaves somePublicMethod $this->assertMethodWoven(FinalClass::class, 'somePublicMethod'); /* @see \Go\Tests\TestProject\Application\FinalClass::someFinalPublicMethod() */ // it should match and weave someFinalPublicMethod $this->assertMethodWoven(FinalClass::class, 'someFinalPublicMethod'); /* @see \Go\Tests\TestProject\Application\ParentWithFinalMethod::someFinalParentMethod() */ // it should not match with parent final method in the class $this->assertMethodNotWoven(FinalClass::class, 'someFinalParentMethod'); } public function testItDoesNotWeaveInterfaces() { $this->assertClassIsNotWoven(FooInterface::class); } } tests/Go/Functional/Issue293Test.php000064400000001677144760133060013273 0ustar00assertClassIsWoven(Issue293StaticMembers::class); $this->assertStaticMethodWoven(Issue293StaticMembers::class, 'doSomething', 'advisor.Go\\Tests\\TestProject\\Aspect\\Issue293Aspect->afterPublicOrProtectedStaticMethods'); $this->assertStaticMethodWoven(Issue293StaticMembers::class, 'doSomethingElse', 'advisor.Go\\Tests\\TestProject\\Aspect\\Issue293Aspect->afterPublicOrProtectedStaticMethods'); // it does not weaves Issue293DynamicMembers class $this->assertClassIsNotWoven(Issue293DynamicMembers::class); } } tests/Go/Functional/ReflectionFilenameTest.php000064400000002702144760133060015466 0ustar00configuration['frontController']; $path = stream_resolve_include_path($loader); if (!is_readable($path)) { throw new \InvalidArgumentException("Invalid loader path: {$loader}"); } ob_start(); include_once $path; ob_end_clean(); if (!class_exists(AspectKernel::class, false)) { $message = "Kernel was not initialized yet, please configure it in the {$path}"; throw new \InvalidArgumentException($message); } } /** * {@inheritdoc} */ public function tearDown() { parent::tearDown(); $reflectedClass = new \ReflectionClass(FilterInjectorTransformer::class); $reflectedProperty = $reflectedClass->getProperty('kernel'); $reflectedProperty->setAccessible(true); $reflectedProperty = $reflectedProperty->setValue(null); } public function testReflectionFilenameIsCorrect() { $filename = (new ReflectionClass(Main::class))->getFileName(); $main = new Main(); $this->assertEquals($filename, $main->getFilename()); } } tests/Go/Instrument/FileSystem/EnumeratorTest.php000064400000006641144760133060016214 0ustar00mount(); $testPaths = [ '/base/sub/test', '/base/sub/sub/test' ]; // Setup some files we test against foreach ($testPaths as $path) { mkdir('vfs://' . $path, 0777, true); touch('vfs://' . $path . DIRECTORY_SEPARATOR . 'TestClass.php'); } } public static function tearDownAfterClass() { static::$fileSystem->unmount(); } /** * @return array */ public function pathsProvider(): array { return [ [ // No include or exclude, every folder should be there ['vfs://base/sub/test', 'vfs://base/sub/sub/test'], [], [] ], [ // Exclude double sub folder ['vfs://base/sub/test'], [], ['vfs://base/sub/sub/test'] ], [ // Exclude double sub folder just by base path ['vfs://base/sub/test'], [], ['vfs://base/sub/sub'] ], [ // Exclude all, expected shout be empty [], [], ['vfs://base/sub/test', 'vfs://base/sub/sub/test'] ], [ // Exclude all sub using wildcard [], [], ['vfs://base/*/test'] ], [ // Includepath using wildcard should not break ['vfs://base/sub/test', 'vfs://base/sub/sub/test'], ['vfs://base/*'], [] ] ]; } /** * Test wildcard path matching for Enumerator. * * @dataProvider pathsProvider * * @param array $expectedPaths * @param array $includePaths * @param array $excludePaths * @throws \InvalidArgumentException * @throws \LogicException * @throws \UnexpectedValueException */ public function testExclude($expectedPaths, $includePaths, $excludePaths): void { $testPaths = []; /** @var Enumerator $mock */ $mock = $this->getMockBuilder(Enumerator::class) ->setConstructorArgs(['vfs://base', $includePaths, $excludePaths]) ->setMethods(['getFileFullPath']) ->getMock(); // Mock getFileRealPath method to provide a pathname // VFS does not support getRealPath() $mock->expects($this->any()) ->method('getFileFullPath') ->will($this->returnCallback(function (\SplFileInfo $file) { return $file->getPathname(); })); $iterator = $mock->enumerate(); foreach ($iterator as $file) { $testPaths[] = str_replace(DIRECTORY_SEPARATOR, '/', $file->getPath()); } sort($testPaths); sort($expectedPaths); $this->assertEquals($expectedPaths, $testPaths); } } tests/Go/Instrument/PathResolverTest.php000064400000004747144760133060014432 0ustar00assertEquals(__DIR__, PathResolver::realpath(__DIR__, true)); $this->assertEquals(false, PathResolver::realpath(__DIR__ . '/bad/dir', true)); } /** * Test multiple resolve */ public function testCanResolveArray(): void { $this->assertEquals([__DIR__ , __FILE__], PathResolver::realpath([__DIR__ , __FILE__])); } /** * Test for checking the logic of custom realpath() implementation * * @param string $path Given path * @param string $expected Expected path * * @dataProvider realpathExamples */ public function testRealpathWorkingCorrectly($path, $expected): void { // Trick to get scheme name and path in one action. If no scheme, then there will be only one part $components = explode('://', $expected, 2); list ($pathScheme, $localPath) = isset($components[1]) ? $components : [null, $components[0]]; // resolve path parts (single dot, double dot and double delimiters) $localPath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $localPath); if ($pathScheme) { $localPath = "$pathScheme://$localPath"; } $actual = PathResolver::realpath($path); $this->assertEquals($localPath, $actual); } /** * Test paths provider * * @return array */ public function realpathExamples(): array { $curDir = getcwd(); $parent = dirname($curDir); return [ ['/some/absolute/file' , '/some/absolute/file'], ['/some/absolute/file/../points/' , '/some/absolute/points/'], ['/some/./point.php' , '/some/point.php'], ['relative/to/the/dir' , "$curDir/relative/to/the/dir"], ['../relative/filename' , "$parent/relative/filename"], ['./point/file' , "$curDir/point/file"], ['C:\\Windows\\..\\filename', 'C:\\filename'], ['http://localhost/file.name' , 'http://localhost/file.name'], ['http://localhost/some/../relative.file' , 'http://localhost/relative.file'], ['phar://go.phar/some/path' , 'phar://go.phar/some/path'], ['phar://go.phar/some/../relative.file' , 'phar://go.phar/relative.file'], ]; } } tests/Go/Instrument/Transformer/ConstructorExecutionTransformerTest.php000064400000007227144760133060022746 0ustar00"); self::$transformer->transform($metadata); $output = ""; $this->assertEquals($output, $metadata->source); fclose($stream); } public function listOfExpressions(): array { return [ [ '$a = new stdClass', '$a = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{stdClass::class}' ], [ '$b = new stdClass()', '$b = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{stdClass::class}()' ], [ '$stdClass = "stdClass"; $c = new $stdClass', '$stdClass = "stdClass"; $c = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{$stdClass}' ], [ '$stdClass = "stdClass"; $d = new $stdClass()', '$stdClass = "stdClass"; $d = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{$stdClass}()' ], [ '$e = new \Exception', '$e = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{\Exception::class}' ], [ '$f = new \Exception("Test")', '$f = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{\Exception::class}("Test")' ], [ '$g = new self', '$g = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{self::class}', ], [ '$h = new static()', '$h = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{static::class}()' ], [ '$j = new self::$stdClass()', '$j = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{self::$stdClass}()' ], [ '$k = new static::$exception["Exception"]("Test")', '$k = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{static::$exception["Exception"]}("Test")' ], [ '$l = new self::$object[0]->name("Test Message")', '$l = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{self::$object[0]->name}("Test Message")' ], [ '$m = new static::$object[0]->name', '$m = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{static::$object[0]->name}' ], [ '$n = new stdClass(new static::$object[0]->name)', '$n = \Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{stdClass::class}(\Go\Instrument\Transformer\ConstructorExecutionTransformer::getInstance()->{static::$object[0]->name})' ] ]; } } tests/Go/Instrument/Transformer/FilterInjectorTransformerTest.php000064400000012437144760133060021457 0ustar00getKernelMock( [ 'cacheDir' => null, 'cacheFileMode' => 0770, 'appDir' => '', 'debug' => false, 'features' => 0 ], $this->createMock(GoAspectContainer::class) ); $cachePathManager = $this ->getMockBuilder(CachePathManager::class) ->setConstructorArgs([$kernelMock]) ->getMock(); self::$transformer = new FilterInjectorTransformer($kernelMock, 'unit.test', $cachePathManager); } } /** * Returns a mock for kernel * * @return \PHPUnit_Framework_MockObject_MockObject|\Go\Core\AspectKernel */ protected function getKernelMock($options, $container) { $mock = $this->getMockForAbstractClass( AspectKernel::class, [], '', false, true, true, ['getOptions', 'getContainer'] ); $mock ->method('getOptions') ->willReturn($options); $mock ->method('getContainer') ->willReturn($container); return $mock; } public function testCanTransformWithoutInclusion(): void { $metadata = new StreamMetaData(fopen('php://input', 'rb'), ''); $output = $metadata->source; self::$transformer->transform($metadata); $this->assertEquals($output, $metadata->source); } public function testSkipTransformationQuickly(): void { $metadata = new StreamMetaData(fopen('php://input', 'rb'), ''); $output = $metadata->source; self::$transformer->transform($metadata); $this->assertEquals($output, $metadata->source); } public function testCanTransformInclude(): void { $metadata = new StreamMetaData(fopen('php://input', 'rb'), ''); self::$transformer->transform($metadata); $output = ''; $this->assertEquals($output, $metadata->source); } public function testCanTransformIncludeOnce(): void { $metadata = new StreamMetaData(fopen('php://input', 'rb'), ''); self::$transformer->transform($metadata); $output = ''; $this->assertEquals($output, $metadata->source); } public function testCanTransformRequire(): void { $metadata = new StreamMetaData(fopen('php://input', 'rb'), ''); self::$transformer->transform($metadata); $output = ''; $this->assertEquals($output, $metadata->source); } public function testCanTransformRequireOnce(): void { $metadata = new StreamMetaData(fopen('php://input', 'rb'), ''); self::$transformer->transform($metadata); $output = ''; $this->assertEquals($output, $metadata->source); } public function testCanRewriteWithFilter(): void { $actualPath = FilterInjectorTransformer::rewrite('/path/to/my/class.php'); $expectedPath = FilterInjectorTransformer::PHP_FILTER_READ . 'unit.test/resource=/path/to/my/class.php'; $this->assertEquals($expectedPath, $actualPath); } public function testCanRewriteRelativePathsWithFilter(): void { $actualPath = FilterInjectorTransformer::rewrite('_files/class.php', __DIR__); $expectedPath = FilterInjectorTransformer::PHP_FILTER_READ . 'unit.test/resource=' . PathResolver::realpath(__DIR__ . '/_files/class.php'); $this->assertEquals($expectedPath, $actualPath); } public function testCanRewriteClassesWithToString(): void { $file = new \SplFileInfo(__FILE__); $actual = FilterInjectorTransformer::rewrite($file); $this->assertStringEndsWith(__FILE__, $actual); } public function testCanTransformWithBraces(): void { $fileContent = file_get_contents(__DIR__ . '/_files/yii_style.php'); $metadata = new StreamMetaData(fopen(__DIR__ . '/_files/yii_style.php', 'r'), $fileContent); self::$transformer->transform($metadata); $expectedOutput = file_get_contents(__DIR__ . '/_files/yii_style_output.php'); $this->assertEquals($expectedOutput, $metadata->source); } } tests/Go/Instrument/Transformer/MagicConstantTransformerTest.php000064400000006762144760133060021272 0ustar00transformer = new MagicConstantTransformer( $this->getKernelMock([ 'cacheDir' => __DIR__, 'appDir' => dirname(__DIR__), ]) ); } /** * Returns a mock for kernel * * @return \PHPUnit_Framework_MockObject_MockObject|\Go\Core\AspectKernel */ protected function getKernelMock($options) { $mock = $this->getMockForAbstractClass( AspectKernel::class, [], '', false, true, true, ['getOptions', 'getContainer'] ); $mock->expects($this->any()) ->method('getOptions') ->will( $this->returnValue($options) ); $mock->expects($this->any()) ->method('getContainer') ->will( $this->returnValue($this->createMock(AspectContainer::class)) ); return $mock; } public function testTransformerReturnsWithoutMagicConsts(): void { $metadata = new StreamMetaData(fopen('php://input', 'rb'), ''); $expected = $metadata->source; $this->transformer->transform($metadata); $this->assertSame($expected, $metadata->source); } public function testTransformerCanResolveDirMagicConst(): void { $metadata = new StreamMetaData(fopen(__FILE__, 'rb'), ''); $expected = ''; $this->transformer->transform($metadata); $this->assertEquals($expected, $metadata->source); } public function testTransformerCanResolveFileMagicConst(): void { $metadata = new StreamMetaData(fopen(__FILE__, 'rb'), ''); $expected = ''; $this->transformer->transform($metadata); $this->assertEquals($expected, $metadata->source); } public function testTransformerDoesNotReplaceStringWithConst(): void { $metadata = new StreamMetaData(fopen('php://input', 'rb'), ''); $expected = ''; $this->transformer->transform($metadata); $this->assertEquals($expected, $metadata->source); } public function testTransformerWrapsReflectionFileName(): void { $source = 'getFileName(); ?>'; $metadata = new StreamMetaData(fopen('php://input', 'rb'), $source); $this->transformer->transform($metadata); $this->assertStringEndsWith('::resolveFileName($class->getFileName()); ?>', $metadata->source); } public function testTransformerResolvesFileName(): void { /** @var $class MagicConstantTransformer */ $class = get_class($this->transformer); $this->assertStringStartsWith(dirname(__DIR__), $class::resolveFileName(__FILE__)); } } tests/Go/Instrument/Transformer/SelfValueTransformerTest.php000064400000003337144760133060020421 0ustar00transformer = new SelfValueTransformer( $this->getKernelMock([ 'cacheDir' => __DIR__, 'appDir' => dirname(__DIR__), ]) ); } /** * Returns a mock for kernel * * @return \PHPUnit_Framework_MockObject_MockObject|\Go\Core\AspectKernel */ protected function getKernelMock($options) { $mock = $this->getMockForAbstractClass( AspectKernel::class, [], '', false, true, true, ['getOptions', 'getContainer'] ); $mock ->method('getOptions') ->willReturn($options); $mock ->method('getContainer') ->willReturn($this->createMock(AspectContainer::class)); return $mock; } public function testTransformerReplacesAllSelfPlaces(): void { $testFile = fopen(__DIR__ . '/_files/file-with-self.php', 'rb'); $content = stream_get_contents($testFile); $metadata = new StreamMetaData($testFile, $content); $this->transformer->transform($metadata); $expected = file_get_contents(__DIR__ . '/_files/file-with-self-transformed.php'); $this->assertSame($expected, (string) $metadata->source); } } tests/Go/Instrument/Transformer/WeavingTransformerTest.php000064400000023601144760133060020127 0ustar00mount(); } /** * {@inheritDoc} */ public function setUp() { $container = $this->getContainerMock(); $reader = $this->createMock(Reader::class); $loader = $this ->getMockBuilder(AspectLoader::class) ->setConstructorArgs([$container, $reader]) ->getMock(); $this->adviceMatcher = $this->getAdviceMatcherMock(); $this->kernel = $this->getKernelMock( [ 'appDir' => dirname(__DIR__), 'cacheDir' => 'vfs://', 'cacheFileMode' => 0770, 'includePaths' => [], 'excludePaths' => [] ], $container ); $this->cachePathManager = $this ->getMockBuilder(CachePathManager::class) ->setConstructorArgs([$this->kernel]) ->enableProxyingToOriginalMethods() ->getMock(); $this->transformer = new WeavingTransformer( $this->kernel, $this->adviceMatcher, $this->cachePathManager, $loader ); } /** * It's a caution check that multiple namespaces are not yet supported */ public function testMultipleNamespacesInOneFile(): void { $metadata = $this->loadTest('multiple-ns'); $this->transformer->transform($metadata); $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTest('multiple-ns-woven')->source); $this->assertEquals($expected, $actual); } /** * Do not make anything for code without classes */ public function testEmptyNamespaceInFile(): void { $metadata = $this->loadTest('empty-classes'); $this->transformer->transform($metadata); $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTest('empty-classes')->source); $this->assertEquals($expected, $actual); } /** * Do not make anything for aspect class */ public function testAspectIsSkipped(): void { $metadata = $this->loadTest('aspect'); $this->transformer->transform($metadata); $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTest('aspect')->source); $this->assertEquals($expected, $actual); } /** * Main test case for method with typehint */ public function testWeaverForTypeHint(): void { $metadata = $this->loadTest('class-typehint'); $this->transformer->transform($metadata); $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTest('class-typehint-woven')->source); $this->assertEquals($expected, $actual); $proxyContent = file_get_contents($this->cachePathManager->getCacheDir() . '_proxies/Transformer/_files/class-typehint.php/TestClassTypehint.php'); $this->assertFalse(strpos($proxyContent, '\\\\Exception')); } /** * Check that weaver can work with PHP7 classes */ public function testWeaverForPhp7Class(): void { $metadata = $this->loadTest('php7-class'); $this->transformer->transform($metadata); $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTest('php7-class-woven')->source); $this->assertEquals($expected, $actual); if (preg_match("/AOP_CACHE_DIR . '(.+)';$/", $actual, $matches)) { $actualProxyContent = $this->normalizeWhitespaces(file_get_contents('vfs://' . $matches[1])); $expectedProxyContent = $this->normalizeWhitespaces($this->loadTest('php7-class-proxy')->source); $this->assertEquals($expectedProxyContent, $actualProxyContent); } } /** * Transformer verifies include paths */ public function testTransformerWithIncludePaths(): void { $container = $this->getContainerMock(); $reader = $this->createMock(Reader::class); $loader = $this ->getMockBuilder(AspectLoader::class) ->setConstructorArgs([$container, $reader]) ->getMock(); $kernel = $this->getKernelMock( [ 'appDir' => dirname(__DIR__), 'cacheDir' => 'vfs://', 'includePaths' => [__DIR__], 'excludePaths' => [], 'cacheFileMode' => 0770, ], $container ); $cachePathManager = $this->getMockBuilder(CachePathManager::class) ->setConstructorArgs([$kernel]) ->enableProxyingToOriginalMethods() ->getMock(); $this->transformer = new WeavingTransformer( $kernel, $this->adviceMatcher, $cachePathManager, $loader ); $metadata = $this->loadTest('class'); $this->transformer->transform($metadata); $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTest('class-woven')->source); $this->assertEquals($expected, $actual); if (preg_match("/AOP_CACHE_DIR . '(.+)';$/", $actual, $matches)) { $actualProxyContent = $this->normalizeWhitespaces(file_get_contents('vfs://' . $matches[1])); $expectedProxyContent = $this->normalizeWhitespaces($this->loadTest('class-proxy')->source); $this->assertEquals($expectedProxyContent, $actualProxyContent); } } /** * Testcase for multiple classes (@see https://github.com/lisachenko/go-aop-php/issues/71) */ public function testMultipleClasses(): void { $metadata = $this->loadTest('multiple-classes'); $this->transformer->transform($metadata); $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTest('multiple-classes-woven')->source); $this->assertEquals($expected, $actual); } /** * Normalizes string context * * @param string $value * @return string */ protected function normalizeWhitespaces($value): string { return strtr( preg_replace('/\s+$/m', '', $value), [ "\r\n" => PHP_EOL, "\n" => PHP_EOL, ] ); } /** * Returns a mock for kernel * * @param array $options Additional options for kernel * @param AspectContainer $container Container instance * @return \PHPUnit_Framework_MockObject_MockObject|\Go\Core\AspectKernel */ protected function getKernelMock($options, $container) { $mock = $this->getMockForAbstractClass( AspectKernel::class, [], '', false, true, true, ['getOptions', 'getContainer', 'hasFeature'] ); $mock->method('getOptions') ->willReturn($options); $mock->method('getContainer') ->willReturn($container); return $mock; } /** * Returns a mock for container * * @return \PHPUnit_Framework_MockObject_MockObject|AdviceMatcher */ protected function getAdviceMatcherMock() { $mock = $this->createPartialMock(AdviceMatcher::class, ['getAdvicesForClass']); $mock ->method('getAdvicesForClass') ->will( $this->returnCallback(function (\ReflectionClass $refClass) { $advices = []; foreach ($refClass->getMethods() as $method) { $advisorId = "advisor.{$refClass->name}->{$method->name}"; $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = true; } return $advices; }) ); return $mock; } /** * * * @param string $name Name of the file to load * * @return StreamMetaData */ private function loadTest($name): StreamMetaData { $fileName = __DIR__ . '/_files/' . $name . '.php'; $stream = fopen('php://filter/string.tolower/resource=' . $fileName, 'r'); $source = file_get_contents($fileName); $metadata = new StreamMetaData($stream, $source); fclose($stream); return $metadata; } /** * Returns a mock for the container * * @return AspectContainer|\PHPUnit_Framework_MockObject_MockObject */ private function getContainerMock() { $container = $this->createMock(AspectContainer::class); $container ->method('getByTag') ->will($this->returnValueMap([ ['advisor', []] ])); return $container; } } tests/Go/Instrument/Transformer/_files/aspect.php000064400000000172144760133060016162 0ustar00 [ 'publicMethod' => [ 'advisor.Test\\ns1\\TestClass->publicMethod', ], 'protectedMethod' => [ 'advisor.Test\\ns1\\TestClass->protectedMethod', ], 'publicStaticMethod' => [ 'advisor.Test\\ns1\\TestClass->publicStaticMethod', ], 'protectedStaticMethod' => [ 'advisor.Test\\ns1\\TestClass->protectedStaticMethod', ], 'publicMethodDynamicArguments' => [ 'advisor.Test\\ns1\\TestClass->publicMethodDynamicArguments', ], 'publicMethodFixedArguments' => [ 'advisor.Test\\ns1\\TestClass->publicMethodFixedArguments', ], 'methodWithSpecialTypeArguments' => [ 'advisor.Test\\ns1\\TestClass->methodWithSpecialTypeArguments', ], ], ]; public function publicMethod() { return self::$__joinPoints['method:publicMethod']->__invoke($this); } protected function protectedMethod() { return self::$__joinPoints['method:protectedMethod']->__invoke($this); } public static function publicStaticMethod() { return self::$__joinPoints['static:publicStaticMethod']->__invoke(static::class); } protected static function protectedStaticMethod() { return self::$__joinPoints['static:protectedStaticMethod']->__invoke(static::class); } public function publicMethodDynamicArguments($a, &$b) { return self::$__joinPoints['method:publicMethodDynamicArguments']->__invoke($this, [$a, &$b]); } public function publicMethodFixedArguments($a, $b, $c = null) { return self::$__joinPoints['method:publicMethodFixedArguments']->__invoke($this, \array_slice([$a, $b, $c], 0, \func_num_args())); } /** * `self` value is handled on AST level via SelfValueTransformer class * @see \Go\Instrument\Transformer\SelfValueTransformer */ public function methodWithSpecialTypeArguments($instance) { return self::$__joinPoints['method:methodWithSpecialTypeArguments']->__invoke($this, [$instance]); } } \Go\Proxy\ClassProxyGenerator::injectJoinPoints(TestClass::class); tests/Go/Instrument/Transformer/_files/class-typehint-woven.php000064400000000346144760133060021011 0ustar00 [ 'stringSth' => [ 'advisor.Test\\ns1\\TestPhp7Class->stringSth', ], 'floatSth' => [ 'advisor.Test\\ns1\\TestPhp7Class->floatSth', ], 'boolSth' => [ 'advisor.Test\\ns1\\TestPhp7Class->boolSth', ], 'intSth' => [ 'advisor.Test\\ns1\\TestPhp7Class->intSth', ], 'callableSth' => [ 'advisor.Test\\ns1\\TestPhp7Class->callableSth', ], 'arraySth' => [ 'advisor.Test\\ns1\\TestPhp7Class->arraySth', ], 'variadicStringSthByRef' => [ 'advisor.Test\\ns1\\TestPhp7Class->variadicStringSthByRef', ], 'exceptionArg' => [ 'advisor.Test\\ns1\\TestPhp7Class->exceptionArg', ], 'stringRth' => [ 'advisor.Test\\ns1\\TestPhp7Class->stringRth', ], 'floatRth' => [ 'advisor.Test\\ns1\\TestPhp7Class->floatRth', ], 'boolRth' => [ 'advisor.Test\\ns1\\TestPhp7Class->boolRth', ], 'intRth' => [ 'advisor.Test\\ns1\\TestPhp7Class->intRth', ], 'callableRth' => [ 'advisor.Test\\ns1\\TestPhp7Class->callableRth', ], 'arrayRth' => [ 'advisor.Test\\ns1\\TestPhp7Class->arrayRth', ], 'exceptionRth' => [ 'advisor.Test\\ns1\\TestPhp7Class->exceptionRth', ], 'noRth' => [ 'advisor.Test\\ns1\\TestPhp7Class->noRth', ], 'returnSelf' => [ 'advisor.Test\\ns1\\TestPhp7Class->returnSelf', ], ], ]; public function stringSth(string $arg) { return self::$__joinPoints['method:stringSth']->__invoke($this, [$arg]); } public function floatSth(float $arg) { return self::$__joinPoints['method:floatSth']->__invoke($this, [$arg]); } public function boolSth(bool $arg) { return self::$__joinPoints['method:boolSth']->__invoke($this, [$arg]); } public function intSth(int $arg) { return self::$__joinPoints['method:intSth']->__invoke($this, [$arg]); } public function callableSth(callable $arg) { return self::$__joinPoints['method:callableSth']->__invoke($this, [$arg]); } public function arraySth(array $arg) { return self::$__joinPoints['method:arraySth']->__invoke($this, [$arg]); } public function variadicStringSthByRef(string &... $args) { return self::$__joinPoints['method:variadicStringSthByRef']->__invoke($this, $args); } public function exceptionArg(\Exception $exception, \Test\ns1\Exception $localException) { return self::$__joinPoints['method:exceptionArg']->__invoke($this, [$exception, $localException]); } public function stringRth(string $arg) : string { return self::$__joinPoints['method:stringRth']->__invoke($this, [$arg]); } public function floatRth(float $arg) : float { return self::$__joinPoints['method:floatRth']->__invoke($this, [$arg]); } public function boolRth(bool $arg) : bool { return self::$__joinPoints['method:boolRth']->__invoke($this, [$arg]); } public function intRth(int $arg) : int { return self::$__joinPoints['method:intRth']->__invoke($this, [$arg]); } public function callableRth(callable $arg) : callable { return self::$__joinPoints['method:callableRth']->__invoke($this, [$arg]); } public function arrayRth(array $arg) : array { return self::$__joinPoints['method:arrayRth']->__invoke($this, [$arg]); } public function exceptionRth(\Exception $exception) : \Exception { return self::$__joinPoints['method:exceptionRth']->__invoke($this, [$exception]); } public function noRth(\Test\ns1\LocalException $exception) { return self::$__joinPoints['method:noRth']->__invoke($this, [$exception]); } /** * `self` value is handled on AST level via SelfValueTransformer class * @see \Go\Instrument\Transformer\SelfValueTransformer */ public function returnSelf() { return self::$__joinPoints['method:returnSelf']->__invoke($this); } } \Go\Proxy\ClassProxyGenerator::injectJoinPoints(TestPhp7Class::class); tests/Go/Instrument/Transformer/_files/php7-class-woven.php000064400000002337144760133060020025 0ustar00method('filename')); include_once($class->method('filename')); require($class->method('filename')); require_once($class->method('filename')); $y = $z ? include $x : $v; include $z ? $x : $y; $app->bindInstallPaths(require __DIR__.'/paths.php'); tests/Go/Instrument/Transformer/_files/yii_style_output.php000064400000001445144760133060020341 0ustar00method('filename')), __DIR__); include_once \Go\Instrument\Transformer\FilterInjectorTransformer::rewrite(($class->method('filename')), __DIR__); require \Go\Instrument\Transformer\FilterInjectorTransformer::rewrite(($class->method('filename')), __DIR__); require_once \Go\Instrument\Transformer\FilterInjectorTransformer::rewrite(($class->method('filename')), __DIR__); $y = $z ? include \Go\Instrument\Transformer\FilterInjectorTransformer::rewrite($x, __DIR__) : $v; include \Go\Instrument\Transformer\FilterInjectorTransformer::rewrite($z ? $x : $y, __DIR__); $app->bindInstallPaths(require \Go\Instrument\Transformer\FilterInjectorTransformer::rewrite(__DIR__.'/paths.php', __DIR__)); tests/Go/PhpUnit/ClassAdvisorIdentifier.php000064400000004234144760133060014762 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\PhpUnit; /** * Value object class for class member weaving constraints. */ final class ClassAdvisorIdentifier { /** * @var string */ private $class; /** * @var string */ private $subject; /** * @var string */ private $target; /** * @var string */ private $advisorIdentifier; /** * @var null|int */ private $index; public function __construct( $class, string $subject, string $target, string $advisorIdentifier = null, int $index = null ) { $this->class = is_object($class) ? get_class($class) : $class; $this->subject = $subject; $this->advisorIdentifier = $advisorIdentifier; $this->target = $target; $this->index = $index; } /** * Get full qualified class name. */ public function getClass(): string { return $this->class; } /** * Get name of subject (method, property...) of interception. */ public function getSubject(): string { return $this->subject; } /** * Get weaving target. * * @see \Go\Core\AspectContainer::METHOD_PREFIX * @see \Go\Core\AspectContainer::PROPERTY_PREFIX * @see \Go\Core\AspectContainer::STATIC_METHOD_PREFIX * @see \Go\Core\AspectContainer::INIT_PREFIX * @see \Go\Core\AspectContainer::STATIC_INIT_PREFIX * * @return string */ public function getTarget(): string { return $this->target; } /** * Get get advisor identifier. * * @return string|null */ public function getAdvisorIdentifier() { return $this->advisorIdentifier; } /** * Get join point ordering index. * * @return int|null */ public function getIndex() { return $this->index; } } tests/Go/PhpUnit/ClassIsNotWovenConstraint.php000064400000002574144760133060015475 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\PhpUnit; use Go\Instrument\PathResolver; use Go\ParserReflection\ReflectionClass; use PHPUnit\Framework\Constraint\Constraint; /** * Asserts that class is not woven. */ final class ClassIsNotWovenConstraint extends Constraint { /** * @var array */ private $configuration; public function __construct(array $configuration) { parent::__construct(); $this->configuration = $configuration; } /** * {@inheritdoc} */ public function matches($other): bool { $filename = (new ReflectionClass($other))->getFileName(); $suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir']))); $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix); $proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix); // if any of files exists, assert has to fail return !$transformedFileExists && !$proxyFileExists; } /** * {@inheritdoc} */ public function toString(): string { return 'is not woven class.'; } } tests/Go/PhpUnit/ClassMemberNotWovenConstraint.php000064400000004004144760133060016317 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\PhpUnit; use PHPUnit\Framework\Constraint\Constraint; /** *Asserts that class member is not woven for given class. */ final class ClassMemberNotWovenConstraint extends Constraint { /** * @var array */ private $configuration; public function __construct(array $configuration) { parent::__construct(); $this->configuration = $configuration; } /** * {@inheritdoc} */ public function matches($other): bool { if (!$other instanceof ClassAdvisorIdentifier) { throw new \InvalidArgumentException(sprintf('Expected instance of "%s", got "%s".', ClassAdvisorIdentifier::class, is_object($other) ? get_class($other) : gettype($other))); } $reflectionClass = ProxyClassReflectionHelper::createReflectionClass($other->getClass(), $this->configuration); $wovenAdvisorIdentifiers = $reflectionClass->getStaticPropertyValue('__joinPoints', null); $target = $other->getTarget(); if (null === $wovenAdvisorIdentifiers) { // there are no advisor identifiers return true; } if (!isset($wovenAdvisorIdentifiers[$target])) { return true; } if (!isset($wovenAdvisorIdentifiers[$target][$other->getSubject()])) { return true; } if (null === $other->getAdvisorIdentifier()) { return false; // if advisor identifier is not specified, that means that any matches, so weaving exists. } return !in_array($other->getAdvisorIdentifier(), $wovenAdvisorIdentifiers[$target][$other->getSubject()], true); } /** * {@inheritdoc} */ public function toString(): string { return 'class member not woven.'; } } tests/Go/PhpUnit/ClassMemberWovenConstraint.php000064400000004267144760133060015651 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\PhpUnit; use PHPUnit\Framework\Constraint\Constraint; /** * Asserts that class member is woven for given class. */ final class ClassMemberWovenConstraint extends Constraint { /** * @var array */ private $configuration; public function __construct(array $configuration) { parent::__construct(); $this->configuration = $configuration; } /** * {@inheritdoc} */ public function matches($other): bool { if (!$other instanceof ClassAdvisorIdentifier) { throw new \InvalidArgumentException(sprintf('Expected instance of "%s", got "%s".', ClassAdvisorIdentifier::class, is_object($other) ? get_class($other) : gettype($other))); } $reflectionClass = ProxyClassReflectionHelper::createReflectionClass($other->getClass(), $this->configuration); $wovenAdvisorIdentifiers = $reflectionClass->getStaticPropertyValue('__joinPoints', null); $target = $other->getTarget(); if (null === $wovenAdvisorIdentifiers) { // there are no advisor identifiers return false; } if (!isset($wovenAdvisorIdentifiers[$target])) { return false; } if (!isset($wovenAdvisorIdentifiers[$target][$other->getSubject()])) { return false; } if (null === $other->getAdvisorIdentifier()) { return true; // if advisor identifier is not specified, that means that any matches, so weaving exists. } $index = $other->getIndex(); $advisorIndex = array_search($other->getAdvisorIdentifier(), $wovenAdvisorIdentifiers[$target][$other->getSubject()], true); $isIndexValid = ($index === null) || ($advisorIndex === $index); return $advisorIndex !== false && $isIndexValid; } /** * {@inheritdoc} */ public function toString(): string { return 'class member woven.'; } } tests/Go/PhpUnit/ClassWovenConstraint.php000064400000002565144760133060014520 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\PhpUnit; use Go\Instrument\PathResolver; use Go\ParserReflection\ReflectionClass; use PHPUnit\Framework\Constraint\Constraint; /** * Asserts that class is not woven. */ final class ClassWovenConstraint extends Constraint { /** * @var array */ private $configuration; public function __construct(array $configuration) { parent::__construct(); $this->configuration = $configuration; } /** * {@inheritdoc} */ public function matches($other): bool { $filename = (new ReflectionClass($other))->getFileName(); $suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir']))); $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix); $proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix); // if any of files is missing, assert has to fail return $transformedFileExists && $proxyFileExists; } /** * {@inheritdoc} */ public function toString(): string { return 'is woven class.'; } } tests/Go/PhpUnit/ProxyClassReflectionHelper.php000064400000004600144760133060015641 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\PhpUnit; use Go\Instrument\PathResolver; use Go\ParserReflection\ReflectionClass; use Go\ParserReflection\ReflectionEngine; use Go\ParserReflection\ReflectionFile; /** * Utility functions that helps initialization of reflection classes that introspects classes and its members * by parsing its AST (without loading class into memory). */ final class ProxyClassReflectionHelper { private function __construct() { // noop } /** * Creates \Go\ParserReflection\ReflectionClass instance that introspects class without loading class into memory. * * @param string $className Full qualified class name for which \Go\ParserReflection\ReflectionClass ought to be initialized * @param array $configuration Configuration used for Go! AOP project setup * @return ReflectionClass */ public static function createReflectionClass(string $className, array $configuration): ReflectionClass { $parsedReflectionClass = new ReflectionClass($className); $originalClassFile = $parsedReflectionClass->getFileName(); $originalNamespace = $parsedReflectionClass->getNamespaceName(); $appDir = PathResolver::realpath($configuration['appDir']); $relativePath = str_replace($appDir . DIRECTORY_SEPARATOR, '', $originalClassFile); $classSuffix = str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; $proxyRelativePath = $relativePath . DIRECTORY_SEPARATOR . $classSuffix; $proxyFileName = $configuration['cacheDir'] . '/_proxies/' . $proxyRelativePath; $proxyFileContent = file_get_contents($proxyFileName); // To prevent deep analysis of parents, we just cut everything after "extends" $proxyFileContent = preg_replace('/extends.*/', '', $proxyFileContent); $proxyFileAST = ReflectionEngine::parseFile($proxyFileName, $proxyFileContent); $proxyReflectionFile = new ReflectionFile($proxyFileName, $proxyFileAST); $proxyClassReflection = $proxyReflectionFile->getFileNamespace($originalNamespace)->getClass($className); return $proxyClassReflection; } } tests/Go/Proxy/ClassProxyGeneratorTest.php000064400000006654144760133060014742 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy; use Go\ParserReflection\ReflectionEngine; use Go\ParserReflection\ReflectionFile; use Go\Proxy\Part\JoinPointPropertyGenerator; use Go\Stubs\First; use PHPUnit\Framework\TestCase; use ReflectionClass; use function substr; /** * Test case for generated function definition */ class ClassProxyGeneratorTest extends TestCase { /** * Test proxy generation for class method * * @param string $className Name of the class to intercept * @param string $methodName Name of the method to intercept * * @throws \Go\ParserReflection\ReflectionException * @throws \ReflectionException * * @dataProvider dataGenerator */ public function testGenerateProxyMethod(string $className, string $methodName): void { $reflectionClass = new ReflectionClass($className); $classAdvices = [ 'method' => [ $methodName => ['test'] ] ]; $childGenerator = new ClassProxyGenerator( $reflectionClass, 'Test', $classAdvices, false ); $proxyFileContent = "generate(); // To prevent deep analysis of parents, we just cut everything after "extends" $proxyFileContent = preg_replace('/extends.*/', '', $proxyFileContent); $proxyFileAST = ReflectionEngine::parseFile('/dev/null', $proxyFileContent); $proxyFile = new ReflectionFile('/dev/null', $proxyFileAST); $namespaces = $proxyFile->getFileNamespaces(); // Generated proxy should contain only one single namespace for all test cases $this->assertCount(1, $namespaces); $expectedNamespace = $reflectionClass->getNamespaceName(); $this->assertSame($expectedNamespace, $namespaces[$expectedNamespace]->getName()); // We should have exactly one class with the same name as original one $proxyClass = $namespaces[$expectedNamespace]->getClass($className); $proxyHasJoinpointProperty = $proxyClass->hasProperty(JoinPointPropertyGenerator::NAME); $this->assertTrue($proxyHasJoinpointProperty, 'Child should have joinpoint property in it'); $joinPoints = $proxyClass->getStaticPropertyValue(JoinPointPropertyGenerator::NAME); $this->assertSame($classAdvices, $joinPoints); $this->assertTrue($proxyClass->hasMethod($methodName)); $interceptedMethod = $proxyClass->getMethod($methodName); $methodStartPos = $interceptedMethod->getNode()->stmts[0]->getAttribute('startFilePos'); $methodEndPos = $interceptedMethod->getNode()->stmts[0]->getAttribute('endFilePos'); $methodBody = substr($proxyFileContent, $methodStartPos, ($methodEndPos-$methodStartPos)); $this->assertStringStartsWith("return self::\$__joinPoints['method:{$methodName}']->__invoke(", $methodBody); } /** * Provides list of methods with expected attributes * * @return array */ public function dataGenerator(): array { return [ [First::class, 'publicMethod'], [First::class, 'protectedMethod'], [First::class, 'passByReference'], ]; } } tests/Go/Proxy/Part/FunctionCallArgumentListGeneratorTest.php000064400000003071144760133060020447 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use PHPUnit\Framework\TestCase; class FunctionCallArgumentListGeneratorTest extends TestCase { /** * Tests that generator can generate function call argument list * * @param string $functionName Function to reflect * @dataProvider dataGenerator * @throws \ReflectionException if function is not present */ public function testGenerate(string $functionName, string $expectedLine): void { $reflection = new \ReflectionFunction($functionName); $generator = new FunctionCallArgumentListGenerator($reflection); $actualLine = $generator->generate(); $this->assertSame($expectedLine, $actualLine); } /** * Provides list of functions with expected generated code for calling such functions * * @return array */ public function dataGenerator(): array { return [ ['var_dump', '$vars'], // var_dump(...$vars) ['array_pop', '[&$stack]'], // array_pop(&$stack) ['pack', '[$format], $args'], // pack($format, ...$args) ['strcoll', '[$str1, $str2]'], // strcoll($str1, $str2) ['basename', '\array_slice([$path, $suffix], 0, \func_num_args())'], // basename($path, $suffix = null) ]; } } tests/Go/Proxy/Part/FunctionParameterListTest.php000064400000007155144760133060016151 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use function class_exists; use PHPUnit\Framework\TestCase; use Zend\Code\Generator\ValueGenerator; class FunctionParameterListTest extends TestCase { /** * Tests that generated from reflection parameter list is correct * * @param string $functionName Function to reflect * @param int $expectedArgsNumber Number of expected arguments * @param array $checks List of checks, where key is argument number, starting from 0 and value are getters * * @throws \ReflectionException if function is not present * @dataProvider dataGenerator */ public function testgetGeneratedParameters(string $functionName, int $expectedArgsNumber, array $checks): void { $reflection = new \ReflectionFunction($functionName); $parameterList = new FunctionParameterList($reflection); $generatedList = $parameterList->getGeneratedParameters(); $this->assertCount($expectedArgsNumber, $generatedList); foreach ($checks as $argumentNumber => $argumentChecks) { $generatedArg = $generatedList[$argumentNumber]; foreach ($argumentChecks as $argumentCheck => $expectedValue) { $actualValue = $generatedArg->$argumentCheck(); if (is_string($expectedValue) && class_exists($expectedValue, false)) { $this->assertInstanceOf($expectedValue, $actualValue); } else { $this->assertSame($expectedValue, $actualValue); } } } } /** * Provides list of functions with expected generated args * * @return array */ public function dataGenerator(): array { return [ [ 'var_dump', // var_dump(...$vars) 1, [ [ 'getPassedByReference' => false, 'getVariadic' => true, 'getName' => 'vars', 'getDefaultValue' => null, ] ] ], [ 'array_pop', // array_pop(&$stack) 1, [ [ 'getPassedByReference' => true, 'getVariadic' => false, 'getName' => 'stack', 'getDefaultValue' => null, ] ] ], [ 'strcoll', // strcoll($str1, $str2) 2, [ [ 'getPassedByReference' => false, 'getVariadic' => false, 'getName' => 'str1', 'getDefaultValue' => null, ] ] ], [ 'microtime', // microtime($get_as_float = null) 1, [ [ 'getPassedByReference' => false, 'getVariadic' => false, 'getName' => 'get_as_float', 'getDefaultValue' => ValueGenerator::class ] ] ] ]; } } tests/Go/Proxy/Part/InterceptedConstructorGeneratorTest.php000064400000010115144760133060020240 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use Exception; use Go\Stubs\Constructor\ClassWithOptionalArgsConstructor; use Go\Stubs\Constructor\ClassWithoutConstructor; use Go\Stubs\Constructor\ClassWithPrivateConstructor; use Go\Stubs\Constructor\ClassWithProtectedConstructor; use PHPUnit\Framework\TestCase; use function preg_replace; use ReflectionClass; /** * Test case for generated method definition */ class InterceptedConstructorGeneratorTest extends TestCase { /** * Tests that generator can generate valid method definition * * @param string $className * @param string $expectedSignature * * @throws \ReflectionException * @dataProvider dataGenerator */ public function testGenerate(string $className, string $expectedSignature): void { $reflectionConstructor = (new ReflectionClass($className))->getConstructor(); $generator = new InterceptedConstructorGenerator([], $reflectionConstructor); $generatedCode = $generator->generate(); $generatedCode = preg_replace('/^\s+/m', '', $generatedCode); $expectedCode = preg_replace('/^\s+/m', '', $expectedSignature); $this->assertStringMatchesFormat($expectedCode, $generatedCode); } /** * Provides list of methods with expected attributes * * @return array */ public function dataGenerator(): array { return [ [ Exception::class, 'public function __construct($message = null, $code = null, $previous = null) { parent::__construct(...\array_slice([$message, $code, $previous], 0, \func_num_args())); }' ], [ ClassWithOptionalArgsConstructor::class, 'public function __construct(int $foo = 42, bool $bar = false, \stdClass $instance = null) { parent::__construct(...\array_slice([$foo, $bar, $instance], 0, \func_num_args())); }' ], [ ClassWithoutConstructor::class, 'public function __construct() { }' ], [ ClassWithProtectedConstructor::class, 'protected function __construct(string $className, int &$byReference) { parent::__construct(...[$className, &$byReference]); }' ], ]; } public function testGenerateWithProperties(): void { $reflectionConstructor = (new ReflectionClass(ClassWithoutConstructor::class))->getConstructor(); $generator = new InterceptedConstructorGenerator(['foo', 'bar'], $reflectionConstructor); $generatedCode = $generator->generate(); $generatedCode = preg_replace('/^\s+|\s+$/m', '', $generatedCode); $expectedCode = preg_replace('/^\s+|\s+$/m', '', ' public function __construct() { $accessor = function(array &$propertyStorage, object $target) { $propertyStorage = [ \'foo\' => &$target->foo, \'bar\' => &$target->bar ]; unset( $target->foo, $target->bar ); }; ($accessor->bindTo($this, parent::class))($this->__properties, $this); }' ); $this->assertSame($expectedCode, $generatedCode); } public function testThrowsExceptionForPrivateConstructor(): void { $this->expectException(\LogicException::class); $reflectionConstructor = (new ReflectionClass(ClassWithPrivateConstructor::class))->getConstructor(); new InterceptedConstructorGenerator(['foo', 'bar'], $reflectionConstructor); } } tests/Go/Proxy/Part/InterceptedFunctionGeneratorTest.php000064400000004501144760133060017502 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use PHPUnit\Framework\TestCase; use function preg_replace; /** * Contains test function with return type to be reflected * * @return \Exception */ function funcWithReturnTypeAndDocBlock(): \Exception { return new \Exception('Test'); } /** * Test case for generated function definition */ class InterceptedFunctionGeneratorTest extends TestCase { /** * Tests that generator can generate valid function definition * * @param string $functionName Name of the function to reflect * @param string $expectedSignature Expected function signature * * @throws \ReflectionException * @dataProvider dataGenerator */ public function testGenerate(string $functionName, string $expectedSignature): void { $reflectionFunction = new \ReflectionFunction($functionName); $generator = new InterceptedFunctionGenerator($reflectionFunction, "\n"); $generatedCode = $generator->generate(); // Clean PhpDoc comment, @see https://stackoverflow.com/a/4207149/801258 $generatedCode = preg_replace('#/\*.+?\*/#s', '', $generatedCode); // Remove trailing spaces and empty function body $generatedCode = trim($generatedCode, "\n{} "); $this->assertSame($expectedSignature, $generatedCode); } /** * Provides list of methods with expected attributes * * @return array */ public function dataGenerator(): array { return [ [ 'var_dump', 'function var_dump(... $vars)' ], [ 'array_pop', 'function array_pop(&$stack)' ], [ 'strcoll', 'function strcoll($str1, $str2)' ], [ 'microtime', 'function microtime($get_as_float = null)' ], [ '\Go\Proxy\Part\funcWithReturnTypeAndDocBlock', 'function funcWithReturnTypeAndDocBlock() : \Exception' ], ]; } } tests/Go/Proxy/Part/InterceptedMethodGeneratorTest.php000064400000004522144760133060017140 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use Go\Stubs\First; use PHPUnit\Framework\TestCase; use function preg_replace; /** * Test case for generated method definition */ class InterceptedMethodGeneratorTest extends TestCase { /** * Tests that generator can generate valid method definition * * @param string $className * @param string $methodName * @param string $expectedSignature * * @throws \ReflectionException * @dataProvider dataGenerator */ public function testGenerate(string $className, string $methodName, string $expectedSignature): void { $reflectionMethod = new \ReflectionMethod($className, $methodName); $generator = new InterceptedMethodGenerator($reflectionMethod, ''); $generatedCode = $generator->generate(); // Clean PhpDoc comment, @see https://stackoverflow.com/a/4207149/801258 $generatedCode = preg_replace('#/\*.+?\*/#s', '', $generatedCode); // Remove trailing spaces and empty function body $generatedCode = trim($generatedCode, "\n{} "); $this->assertSame($expectedSignature, $generatedCode); } /** * Provides list of methods with expected attributes * * @return array */ public function dataGenerator(): array { return [ [ First::class, 'variadicArgsTest', 'public function variadicArgsTest(... $args)' ], [ First::class, 'staticLsbRecursion', 'public static function staticLsbRecursion($value, $level = 0)' ], [ First::class, 'staticLsbProtected', 'protected static function staticLsbProtected()' ], [ First::class, 'passByReference', 'public function passByReference(&$valueByReference)' ], [ First::class, 'privateMethod', 'private function privateMethod()' ], ]; } } tests/Go/Proxy/Part/JoinPointPropertyGeneratorTest.php000064400000004133144760133060017205 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ namespace Go\Proxy\Part; use PHPUnit\Framework\TestCase; /** * Test case for joinPoint property generator */ class JoinPointPropertyGeneratorTest extends TestCase { /** * Tests that generator can generate valid joinpoint property */ public function testGenerate(): void { $generator = new JoinPointPropertyGenerator([ 'method' => [ 'execute' => [ 'advisor.Demo\\Aspect\\LoggingAspect->beforeMethodExecution', ], 'perform' => [ 'advisor.Demo\\Aspect\\LoggingAspect->beforeMethodExecution', ], ], 'static' => [ 'runByName' => [ 'advisor.Demo\\Aspect\\LoggingAspect->beforeMethodExecution', ], ], ]); $this->assertSame('__joinPoints', $generator->getName()); $propertyCode = $generator->generate(); $expectedCode = preg_replace( '/^\s+|\s+$/m', '', '/** * List of applied advices per class */ private static $__joinPoints = [ \'method\' => [ \'execute\' => [ \'advisor.Demo\\\\Aspect\\\\LoggingAspect->beforeMethodExecution\', ], \'perform\' => [ \'advisor.Demo\\\\Aspect\\\\LoggingAspect->beforeMethodExecution\', ], ], \'static\' => [ \'runByName\' => [ \'advisor.Demo\\\\Aspect\\\\LoggingAspect->beforeMethodExecution\', ], ], ];' ); $actualCode = preg_replace('/^\s+|\s+$/m', '', $propertyCode); $this->assertSame($expectedCode, $actualCode); } } tests/Go/Stubs/AbstractInterceptorMock.php000064400000002063144760133060014665 0ustar00 'aspect', 'method' => 'Go\Aop\Framework\{closure}', 'aspect' => 'Go\Aop\Framework\BaseInterceptorTest' ]; } /** * {@inheritdoc} */ public static function unserializeAdvice(array $adviceData) : Closure { return self::$advice; } /** * {@inheritdoc} */ public function invoke(Joinpoint $joinpoint) { return $joinpoint; } } tests/Go/Stubs/Constructor/ClassWithOptionalArgsConstructor.php000064400000000326144760133060021110 0ustar00 properties private function privateMethod() { return $this->private; } protected function protectedMethod() { return $this->protected; } public function publicMethod() { return $this->public; } // Static methods that access self:: properties private static function staticSelfPrivate() { return self::$staticPrivate; } protected static function staticSelfProtected() { return self::$staticProtected; } public static function staticSelfPublic() { return self::$staticPublic; } public static function staticSelfPublicAccessPrivate() { return self::$staticPrivate; } // Static methods that access static:: properties with LSB protected static function staticLsbProtected() { return get_called_class(); } public static function staticLsbPublic() { return get_called_class(); } // Pass by reference public function passByReference(&$valueByReference) { $valueByReference = null; return $valueByReference; } // Pass by reference public static function staticPassByReference(&$valueByReference) { $valueByReference = null; return $valueByReference; } // Recursion test public function recursion($value, $level = 0) { if ($level > 0) { $value += $this->recursion($value, $level-1); } return $value; } // Recursion test public static function staticLsbRecursion($value, $level = 0) { if ($level > 0) { $value += static::staticLsbRecursion($value, $level-1); } return $value; } /** * Method for checking invocation with any number of arguments * * NB: Real proxy use the method definition to prepare invocation proxy, so variable number of arguments * will not work at all! * * @return string */ public function variableArgsTest() { return implode('', func_get_args()); } /** * Method for checking variadic arguments * * @return array */ public function variadicArgsTest(...$args) { return implode('', $args); } /** * Method for checking invocation with any number of arguments * * NB: Real proxy use the method definition to prepare invocation proxy, so variable number of arguments * will not work at all! * * @return string */ public static function staticVariableArgsTest() { return implode('', func_get_args()); } /** * Method for checking static variadic arguments * * @return array */ public static function staticVariadicArgsTest(...$args) { return implode('', $args); } } tests/Go/Stubs/FirstStatic.php000064400000000753144760133060012334 0ustar00__invoke(__CLASS__, [$value, $level]); } } tests/bootstrap.php000064400000000513144760133060010437 0ustar00 * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. */ ini_set('display_errors', 'on'); include __DIR__ . '/../vendor/autoload.php'; tests/functions.php000064400000000676144760133060010444 0ustar00 */ namespace Symfony\Component\Finder; /** * This helper function overrides the PHP glob() function so it is able to be run with virtual file system, * which is supported by Webmozart\Glob\Glob * * @param $pattern * @param null $flags * * @return string[] */ function glob($pattern, $flags = null) { return \Webmozart\Glob\Glob::glob($pattern, $flags); }