├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── infection.json.dist ├── psalm.xml └── src ├── ClassCache.php ├── ClassConfigFactory.php ├── ClassRenderer.php ├── Config ├── ClassConfig.php ├── MethodConfig.php ├── ParameterConfig.php └── TypeConfig.php ├── ObjectProxy.php ├── ProxyManager.php └── ProxyTrait.php /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8.1 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | 11 | enabled: 12 | - alpha_ordered_traits 13 | - array_indentation 14 | - array_push 15 | - combine_consecutive_issets 16 | - combine_consecutive_unsets 17 | - combine_nested_dirname 18 | - declare_strict_types 19 | - dir_constant 20 | - fully_qualified_strict_types 21 | - function_to_constant 22 | - hash_to_slash_comment 23 | - is_null 24 | - logical_operators 25 | - magic_constant_casing 26 | - magic_method_casing 27 | - method_separation 28 | - modernize_types_casting 29 | - native_function_casing 30 | - native_function_type_declaration_casing 31 | - no_alias_functions 32 | - no_empty_comment 33 | - no_empty_phpdoc 34 | - no_empty_statement 35 | - no_extra_block_blank_lines 36 | - no_short_bool_cast 37 | - no_superfluous_elseif 38 | - no_unneeded_control_parentheses 39 | - no_unneeded_curly_braces 40 | - no_unneeded_final_method 41 | - no_unset_cast 42 | - no_unused_imports 43 | - no_unused_lambda_imports 44 | - no_useless_else 45 | - no_useless_return 46 | - normalize_index_brace 47 | - php_unit_dedicate_assert 48 | - php_unit_dedicate_assert_internal_type 49 | - php_unit_expectation 50 | - php_unit_mock 51 | - php_unit_mock_short_will_return 52 | - php_unit_namespaced 53 | - php_unit_no_expectation_annotation 54 | - phpdoc_no_empty_return 55 | - phpdoc_no_useless_inheritdoc 56 | - phpdoc_order 57 | - phpdoc_property 58 | - phpdoc_scalar 59 | - phpdoc_singular_inheritdoc 60 | - phpdoc_trim 61 | - phpdoc_trim_consecutive_blank_line_separation 62 | - phpdoc_type_to_var 63 | - phpdoc_types 64 | - phpdoc_types_order 65 | - print_to_echo 66 | - regular_callable_call 67 | - return_assignment 68 | - self_accessor 69 | - self_static_accessor 70 | - set_type_to_cast 71 | - short_array_syntax 72 | - short_list_syntax 73 | - simplified_if_return 74 | - single_quote 75 | - standardize_not_equals 76 | - ternary_to_null_coalescing 77 | - trailing_comma_in_multiline_array 78 | - unalign_double_arrow 79 | - unalign_equals 80 | - empty_loop_body_braces 81 | - integer_literal_case 82 | - union_type_without_spaces 83 | 84 | disabled: 85 | - function_declaration 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii Proxy Change Log 2 | 3 | ## 1.0.6 under development 4 | 5 | - no changes in this release. 6 | 7 | ## 1.0.5 January 17, 2023 8 | 9 | - Bug #67: Fix unexpected warning in `ClassCache::get()` in some cases (@vjik) 10 | 11 | ## 1.0.4 August 16, 2022 12 | 13 | - Bug #64: Unfinalize `ObjectProxy::__construct()` (@vjik) 14 | 15 | ## 1.0.3 August 15, 2022 16 | 17 | - Bug #59: Fix rendering nullable union types (@vjik) 18 | - Bug #62: Fix rendering intersection types (@vjik) 19 | - Bug #63: Finalize `ObjectProxy::__construct()` (@vjik) 20 | 21 | ## 1.0.2 July 18, 2022 22 | 23 | - Bug #58: Fix rendering of class modifiers (@arogachev) 24 | 25 | ## 1.0.1 July 11, 2022 26 | 27 | - Bug #54: Revert `implements` section for proxy class (@arogachev) 28 | 29 | ## 1.0.0 July 09, 2022 30 | 31 | - Initial release. 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Yii 4 | 5 |

Yii Proxy

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/proxy/v/stable.png)](https://packagist.org/packages/yiisoft/proxy) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/proxy/downloads.png)](https://packagist.org/packages/yiisoft/proxy) 11 | [![Build status](https://github.com/yiisoft/proxy/workflows/build/badge.svg)](https://github.com/yiisoft/proxy/actions?query=workflow%3Abuild) 12 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yiisoft/proxy/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/proxy/?branch=master) 13 | [![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/proxy/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/proxy/?branch=master) 14 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fproxy%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/proxy/master) 15 | [![static analysis](https://github.com/yiisoft/proxy/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/proxy/actions?query=workflow%3A%22static+analysis%22) 16 | [![type-coverage](https://shepherd.dev/github/yiisoft/proxy/coverage.svg)](https://shepherd.dev/github/yiisoft/proxy) 17 | 18 | The package is able to build generic proxy for a class i.e. it allows intercepting all class method calls. It's used in 19 | [yii-debug](https://github.com/yiisoft/yii-debug) package to collect service's method calls information. 20 | 21 | ## Requirements 22 | 23 | - PHP 8.0 or higher. 24 | 25 | ## Installation 26 | 27 | The package could be installed with [Composer](https://getcomposer.org): 28 | 29 | ``` 30 | composer require yiisoft/proxy 31 | ``` 32 | 33 | ## General usage 34 | 35 | ### Custom base proxy class 36 | 37 | Custom base proxy class is useful to perform certain actions during each method call. 38 | 39 | ```php 40 | use Yiisoft\Proxy\ObjectProxy; 41 | 42 | class MyProxy extends ObjectProxy 43 | { 44 | protected function afterCall(string $methodName, array $arguments, mixed $result, float $timeStart) : mixed { 45 | $result = parent::afterCall($methodName, $arguments, $result, $timeStart); 46 | 47 | $error = $this->getCurrentError(); // Use to track and handle errors. 48 | $time = microtime(true) - $timeStart; // Use to measure / log execution time. 49 | 50 | return $result; 51 | } 52 | } 53 | ``` 54 | 55 | Additionally, you can customize new instance creation, etc. See 56 | [examples](https://github.com/yiisoft/yii-debug/tree/master/src/Proxy) in 57 | [yii-debug](https://github.com/yiisoft/yii-debug) extension. 58 | 59 | ### Class with interface 60 | 61 | Having an interface and class implementing it, the proxy can be created like this: 62 | 63 | ```php 64 | use Yiisoft\Proxy\ProxyManager; 65 | 66 | interface CarInterface 67 | { 68 | public function horsepower(): int; 69 | } 70 | 71 | class Car implements CarInterface 72 | { 73 | public function horsepower(): int 74 | { 75 | return 1; 76 | } 77 | } 78 | 79 | $path = sys_get_temp_dir(); 80 | $manager = new ProxyManager( 81 | // This is optional. The proxy can be created "on the fly" instead. But it's recommended to specify path to enable 82 | // caching. 83 | $path 84 | ); 85 | /** @var Car|MyProxy $object */ 86 | $object = $manager->createObjectProxy( 87 | CarInterface::class, 88 | MyProxy::class, // Custom base proxy class defined earlier. 89 | [new Car()] 90 | ); 91 | // Now you can call `Car` object methods through proxy the same as you would call it in original `Car` object. 92 | $object->horsepower(); // Outputs "1". 93 | ``` 94 | 95 | ### Class without interface 96 | 97 | An interface is not required though, the proxy still can be created almost the same way: 98 | 99 | ```php 100 | use Yiisoft\Proxy\ProxyManager; 101 | 102 | class Car implements CarInterface 103 | { 104 | public function horsepower(): int 105 | { 106 | return 1; 107 | } 108 | } 109 | 110 | $path = sys_get_temp_dir(); 111 | $manager = new ProxyManager($path); 112 | /** @var Car|MyProxy $object */ 113 | $object = $manager->createObjectProxy( 114 | Car::class, // Pass class instead of interface here. 115 | MyProxy::class, 116 | [new Car()] 117 | ); 118 | ``` 119 | 120 | ### Proxy class contents 121 | 122 | Here is an example how proxy class looks internally: 123 | 124 | ```php 125 | class CarProxy extends MyProxy implements CarInterface 126 | { 127 | public function horsepower(): int 128 | { 129 | return $this->call('horsepower', []); 130 | } 131 | } 132 | ``` 133 | 134 | ## Documentation 135 | 136 | - [Internals](docs/internals.md) 137 | 138 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. 139 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 140 | 141 | ## License 142 | 143 | The Yii Proxy is free software. It is released under the terms of the BSD License. 144 | Please see [`LICENSE`](./LICENSE.md) for more information. 145 | 146 | Maintained by [Yii Software](https://www.yiiframework.com/). 147 | 148 | ## Support the project 149 | 150 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 151 | 152 | ## Follow updates 153 | 154 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 155 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 156 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 157 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 158 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 159 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/proxy", 3 | "type": "library", 4 | "description": "proxy", 5 | "keywords": [ 6 | "proxy" 7 | ], 8 | "homepage": "https://www.yiiframework.com/", 9 | "license": "BSD-3-Clause", 10 | "support": { 11 | "issues": "https://github.com/yiisoft/proxy/issues?state=open", 12 | "source": "https://github.com/yiisoft/proxy", 13 | "forum": "https://www.yiiframework.com/forum/", 14 | "wiki": "https://www.yiiframework.com/wiki/", 15 | "irc": "ircs://irc.libera.chat:6697/yii", 16 | "chat": "https://t.me/yii3en" 17 | }, 18 | "funding": [ 19 | { 20 | "type": "opencollective", 21 | "url": "https://opencollective.com/yiisoft" 22 | }, 23 | { 24 | "type": "github", 25 | "url": "https://github.com/sponsors/yiisoft" 26 | } 27 | ], 28 | "require": { 29 | "php": "^8.0", 30 | "yiisoft/files": "^1.0.2|^2.0.0" 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit": "^9.5", 34 | "roave/infection-static-analysis-plugin": "^1.16", 35 | "spatie/phpunit-watcher": "^1.23", 36 | "vimeo/psalm": "^4.30|^5.4" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Yiisoft\\Proxy\\": "src" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Yiisoft\\Proxy\\Tests\\": "tests" 46 | } 47 | }, 48 | "scripts": { 49 | "phan": "phan --progress-bar -o analysis.txt", 50 | "test": "phpunit --testdox --no-interaction", 51 | "test-watch": "phpunit-watcher watch" 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "allow-plugins": { 56 | "infection/extension-installer": true, 57 | "composer/package-versions-deprecated": true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "php:\/\/stderr", 9 | "stryker": { 10 | "report": "master" 11 | } 12 | }, 13 | "mutators": { 14 | "@default": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ClassCache.php: -------------------------------------------------------------------------------- 1 | getClassPath($className, $baseProxyClassName), "getClassPath($className, $baseProxyClassName); 59 | if (!file_exists($classPath)) { 60 | return null; 61 | } 62 | 63 | $content = file_get_contents($classPath); 64 | 65 | return $content === false ? null : $content; 66 | } 67 | 68 | /** 69 | * Gets full path to a class. For example: `/tmp/Yiisoft/Tests/Stub/GraphInterface.MyProxy.php` or 70 | * `/tmp/Yiisoft/Tests/Stub/Graph.MyProxy.php`. Additionally, checks and prepares (if needed) {@see $cachePath} for 71 | * usage (@see FileHelper::ensureDirectory()}. 72 | * 73 | * @param string $className The full name of user class or interface (with namespace). For example: `GraphInterface` 74 | * or `Graph`. You can use `::class` instead of manually specifying a string. 75 | * @param string $baseProxyClassName The full name of {@see ObjectProxy} implementation (with namespace) which will 76 | * be the base class for proxy. For example: `MyProxy`. 77 | * 78 | * @throws RuntimeException In case when it's impossible to use or create {@see $cachePath}. 79 | * 80 | * @return string 81 | */ 82 | public function getClassPath(string $className, string $baseProxyClassName): string 83 | { 84 | [$classFileName, $classFilePath] = $this->getClassFileNameAndPath($className, $baseProxyClassName); 85 | 86 | try { 87 | FileHelper::ensureDirectory($classFilePath, 0777); 88 | } catch (RuntimeException) { 89 | throw new RuntimeException("Directory \"$classFilePath\" was not created."); 90 | } 91 | 92 | return $classFilePath . DIRECTORY_SEPARATOR . $classFileName; 93 | } 94 | 95 | /** 96 | * Gets class file name and path as separate elements: 97 | * 98 | * - For name, a combination of both class name and base proxy class name is used. 99 | * - For path, {@see $cachePath} used as a base directory and class namespace for subdirectories. 100 | * 101 | * @param string $className The full name of user class or interface (with namespace). For example: `GraphInterface` 102 | * or `Graph`. You can use `::class` instead of manually specifying a string. 103 | * @param string $baseProxyClassName The full name of {@see ObjectProxy} implementation (with namespace) which will 104 | * be the base class for proxy. For example: `MyProxy`. 105 | * 106 | * @return string[] Array with two elements, the first one is a file name and the second one is a path. For example: 107 | * `[`/tmp/Yiisoft/Proxy/Tests/Stub`, `GraphInterface.MyProxy.php`]` or 108 | * `[`/tmp/Yiisoft/Proxy/Tests/Stub`, `Graph.MyProxy.php`]`. 109 | */ 110 | private function getClassFileNameAndPath(string $className, string $baseProxyClassName): array 111 | { 112 | $classParts = explode('\\', $className); 113 | if (count($classParts) === 1) { 114 | $classParts = ['Builtin', ...$classParts]; 115 | } 116 | 117 | $parentClassParts = explode('\\', $baseProxyClassName); 118 | $classFileName = array_pop($classParts) . '.' . array_pop($parentClassParts) . '.php'; 119 | $classFilePath = $this->cachePath . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $classParts); 120 | 121 | return [$classFileName, $classFilePath]; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/ClassConfigFactory.php: -------------------------------------------------------------------------------- 1 | isInterface(), 56 | namespace: $reflection->getNamespaceName(), 57 | modifiers: Reflection::getModifierNames($reflection->getModifiers()), 58 | name: $reflection->getName(), 59 | shortName: $reflection->getShortName(), 60 | parent: (string) $reflection->getParentClass(), 61 | interfaces: $reflection->getInterfaceNames(), 62 | methods: $this->getMethodConfigs($reflection), 63 | ); 64 | } 65 | 66 | /** 67 | * Gets the complete set of method configs for a given class reflection. 68 | * 69 | * @param ReflectionClass $class Reflection of a class. 70 | * 71 | * @return MethodConfig[] List of method configs. The order is maintained. 72 | * @psalm-return array 73 | */ 74 | private function getMethodConfigs(ReflectionClass $class): array 75 | { 76 | $methods = []; 77 | foreach ($class->getMethods() as $method) { 78 | $methods[$method->getName()] = $this->getMethodConfig($class, $method); 79 | } 80 | 81 | return $methods; 82 | } 83 | 84 | /** 85 | * Gets single method config for individual class / method reflection pair. 86 | * 87 | * @param ReflectionClass $class Reflection of a class. 88 | * @param ReflectionMethod $method Reflection of a method. 89 | * 90 | * @return MethodConfig Single method config. 91 | */ 92 | private function getMethodConfig(ReflectionClass $class, ReflectionMethod $method): MethodConfig 93 | { 94 | return new MethodConfig( 95 | modifiers: $this->getMethodModifiers($class, $method), 96 | name: $method->getName(), 97 | parameters: $this->getMethodParameterConfigs($method), 98 | returnType: $this->getMethodReturnTypeConfig($method), 99 | ); 100 | } 101 | 102 | /** 103 | * Gets the set of method modifiers for a given class / method reflection pair 104 | * 105 | * @param ReflectionClass $class Reflection of a class. 106 | * @param ReflectionMethod $method Reflection of a method. 107 | * 108 | * @return string[] List of method modifiers. 109 | */ 110 | private function getMethodModifiers(ReflectionClass $class, ReflectionMethod $method): array 111 | { 112 | $modifiers = Reflection::getModifierNames($method->getModifiers()); 113 | if (!$class->isInterface()) { 114 | return $modifiers; 115 | } 116 | 117 | return array_values( 118 | array_filter( 119 | $modifiers, 120 | static fn (string $modifier) => $modifier !== 'abstract' 121 | ) 122 | ); 123 | } 124 | 125 | /** 126 | * Gets the complete set of parameter configs for a given method reflection. 127 | * 128 | * @param ReflectionMethod $method Reflection of a method. 129 | * 130 | * @return ParameterConfig[] List of parameter configs. The order is maintained. 131 | * @psalm-return array 132 | */ 133 | private function getMethodParameterConfigs(ReflectionMethod $method): array 134 | { 135 | $parameters = []; 136 | foreach ($method->getParameters() as $param) { 137 | $parameters[$param->getName()] = $this->getMethodParameterConfig($param); 138 | } 139 | 140 | return $parameters; 141 | } 142 | 143 | /** 144 | * Gets single parameter config for individual method's parameter reflection. 145 | * 146 | * @param ReflectionParameter $param Reflection of a method's parameter. 147 | * 148 | * @return ParameterConfig Single parameter config. 149 | */ 150 | private function getMethodParameterConfig(ReflectionParameter $param): ParameterConfig 151 | { 152 | return new ParameterConfig( 153 | type: $this->getMethodParameterTypeConfig($param), 154 | name: $param->getName(), 155 | isDefaultValueAvailable: $param->isDefaultValueAvailable(), 156 | isDefaultValueConstant: $param->isDefaultValueAvailable() 157 | ? $param->isDefaultValueConstant() 158 | : null, 159 | defaultValueConstantName: $param->isOptional() 160 | ? $param->getDefaultValueConstantName() 161 | : null, 162 | defaultValue: $param->isOptional() 163 | ? $param->getDefaultValue() 164 | : null, 165 | ); 166 | } 167 | 168 | /** 169 | * Gets single type config for individual method's parameter reflection. 170 | * 171 | * @param ReflectionParameter $param Reflection pf a method's parameter. 172 | * 173 | * @return TypeConfig|null Single type config. `null` is returned when type is not specified. 174 | */ 175 | private function getMethodParameterTypeConfig(ReflectionParameter $param): ?TypeConfig 176 | { 177 | /** 178 | * @var ReflectionIntersectionType|ReflectionNamedType|ReflectionUnionType|null $type 179 | * @psalm-suppress UndefinedDocblockClass Needed for PHP 8.0 only, because ReflectionIntersectionType is 180 | * not supported. 181 | */ 182 | $type = $param->getType(); 183 | if (!$type) { 184 | return null; 185 | } 186 | 187 | /** 188 | * @psalm-suppress UndefinedClass Needed for PHP 8.0 only, because ReflectionIntersectionType is not supported. 189 | */ 190 | return new TypeConfig( 191 | name: $this->convertTypeToString($type), 192 | allowsNull: $type->allowsNull(), 193 | ); 194 | } 195 | 196 | /** 197 | * Gets single return type config for individual method reflection. 198 | * 199 | * @param ReflectionMethod $method Reflection of a method. 200 | * 201 | * @return TypeConfig|null Single type config. `null` is returned when return type is not specified. 202 | */ 203 | private function getMethodReturnTypeConfig(ReflectionMethod $method): ?TypeConfig 204 | { 205 | $returnType = $method->getReturnType(); 206 | if (!$returnType && method_exists($method, 'getTentativeReturnType')) { 207 | /** 208 | * Needed for PHP 8.0 only, because getTentativeReturnType() is not supported. 209 | * 210 | * @var ReflectionType|null 211 | * @psalm-suppress UnnecessaryVarAnnotation 212 | */ 213 | $returnType = $method->getTentativeReturnType(); 214 | } 215 | 216 | if (!$returnType) { 217 | return null; 218 | } 219 | 220 | /** 221 | * @psalm-suppress ArgumentTypeCoercion Needed for PHP 8.0 only, because ReflectionIntersectionType is 222 | * not supported. 223 | */ 224 | return new TypeConfig( 225 | name: $this->convertTypeToString($returnType), 226 | allowsNull: $returnType->allowsNull(), 227 | ); 228 | } 229 | 230 | /** 231 | * @psalm-suppress UndefinedClass Needed for PHP 8.0 only, because ReflectionIntersectionType is not supported. 232 | */ 233 | private function convertTypeToString( 234 | ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType $type 235 | ): string { 236 | if ($type instanceof ReflectionNamedType) { 237 | return $type->getName(); 238 | } 239 | 240 | if ($type instanceof ReflectionUnionType) { 241 | return $this->getUnionType($type); 242 | } 243 | 244 | return $this->getIntersectionType($type); 245 | } 246 | 247 | private function getUnionType(ReflectionUnionType $type): string 248 | { 249 | $types = array_map( 250 | static fn (ReflectionNamedType $namedType) => $namedType->getName(), 251 | $type->getTypes() 252 | ); 253 | 254 | return implode('|', $types); 255 | } 256 | 257 | /** 258 | * @psalm-suppress UndefinedClass, MixedArgument Needed for PHP 8.0 only, because ReflectionIntersectionType is 259 | * not supported. 260 | */ 261 | private function getIntersectionType(ReflectionIntersectionType $type): string 262 | { 263 | /** 264 | * @psalm-suppress ArgumentTypeCoercion ReflectionIntersectionType::getTypes() always returns 265 | * array of `ReflectionNamedType`, at least until PHP 8.2 released. 266 | */ 267 | $types = array_map( 268 | static fn (ReflectionNamedType $namedType) => $namedType->getName(), 269 | $type->getTypes() 270 | ); 271 | 272 | return implode('&', $types); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/ClassRenderer.php: -------------------------------------------------------------------------------- 1 | call({{methodName}}, [{{params}}]);'; 38 | 39 | /** 40 | * Renders class contents to a string. 41 | * 42 | * @param ClassConfig $classConfig Class config. 43 | * 44 | * @return string Class contents as a string, opening PHP tag is not included. 45 | */ 46 | public function render(ClassConfig $classConfig): string 47 | { 48 | if ($classConfig->isInterface) { 49 | throw new InvalidArgumentException('Rendering of interfaces is not supported.'); 50 | } 51 | 52 | if (!$classConfig->parent) { 53 | throw new InvalidArgumentException('Class config is missing a parent.'); 54 | } 55 | 56 | return trim($this->renderClassSignature($classConfig)) 57 | . "\n" 58 | . '{' 59 | . $this->renderClassBody($classConfig) 60 | . '}'; 61 | } 62 | 63 | /** 64 | * Renders class signature using {@see $classSignatureTemplate}. 65 | * 66 | * @param ClassConfig $classConfig Class config. 67 | * 68 | * @return string Class signature as a string. 69 | */ 70 | private function renderClassSignature(ClassConfig $classConfig): string 71 | { 72 | return strtr($this->classSignatureTemplate, [ 73 | '{{modifiers}}' => $this->renderModifiers($classConfig->modifiers), 74 | '{{name}}' => $classConfig->shortName, 75 | '{{parent}}' => $classConfig->parent, 76 | '{{implements}}' => $this->renderImplements($classConfig->interfaces), 77 | ]); 78 | } 79 | 80 | /** 81 | * Renders implements section. 82 | * 83 | * @param string[] $interfaces A list of interfaces' names with namespaces. 84 | * 85 | * @return string Implements section as a string. Empty string is returned when no interfaces were passed. 86 | * 87 | * @see ClassConfig::$interfaces 88 | */ 89 | private function renderImplements(array $interfaces): string 90 | { 91 | if ($interfaces === []) { 92 | return ''; 93 | } 94 | 95 | return ' implements ' . implode(', ', $interfaces); 96 | } 97 | 98 | /** 99 | * Renders modifiers section. Can be used for both class and method signature. 100 | * 101 | * @param string[] $modifiers A list of modifiers. 102 | * 103 | * @return string Modifiers section as a string. 104 | * 105 | * @see ClassConfig::$modifiers 106 | */ 107 | private function renderModifiers(array $modifiers): string 108 | { 109 | $output = implode(' ', $modifiers); 110 | if ($output !== '') { 111 | $output .= ' '; 112 | } 113 | 114 | return $output; 115 | } 116 | 117 | /** 118 | * Renders class body. 119 | * 120 | * @param ClassConfig $classConfig Class config. 121 | * 122 | * @return string Class body as a string. Empty string is returned when class config has no methods. 123 | */ 124 | private function renderClassBody(ClassConfig $classConfig): string 125 | { 126 | return $this->renderMethods($classConfig->methods); 127 | } 128 | 129 | /** 130 | * Renders all methods. 131 | * 132 | * @param MethodConfig[] $methods A list of method configs. 133 | * 134 | * @return string Methods' sequence as a string. Empty string is returned when no methods were passed. 135 | * 136 | * @see ClassConfig::$methods 137 | */ 138 | private function renderMethods(array $methods): string 139 | { 140 | $methodsCode = ''; 141 | foreach ($methods as $method) { 142 | $methodsCode .= "\n" . $this->renderMethod($method); 143 | } 144 | 145 | return $methodsCode; 146 | } 147 | 148 | /** 149 | * Renders a single method. 150 | * 151 | * @param MethodConfig $method Method config. 152 | * 153 | * @return string Method as a string. 154 | */ 155 | private function renderMethod(MethodConfig $method): string 156 | { 157 | return $this->renderMethodSignature($method) 158 | . "\n" . $this->renderIndent() 159 | . '{' 160 | . $this->renderMethodBody($method) 161 | . $this->renderIndent() 162 | . '}' 163 | . "\n"; 164 | } 165 | 166 | /** 167 | * Renders a proxy method signature using {@see $proxyMethodSignatureTemplate}. 168 | * 169 | * @param MethodConfig $method Method config. 170 | * 171 | * @return string Method signature as a string. 172 | */ 173 | private function renderMethodSignature(MethodConfig $method): string 174 | { 175 | return strtr($this->proxyMethodSignatureTemplate, [ 176 | '{{modifiers}}' => $this->renderIndent() . $this->renderModifiers($method->modifiers), 177 | '{{name}}' => $method->name, 178 | '{{params}}' => $this->renderMethodParameters($method->parameters), 179 | '{{returnType}}' => $this->renderReturnType($method), 180 | ]); 181 | } 182 | 183 | /** 184 | * Renders all parameters for a method. 185 | * 186 | * @param ParameterConfig[] $parameters A list of parameter configs. 187 | * 188 | * @return string Method parameters as a string. Empty string is returned when no parameters were passed. 189 | */ 190 | private function renderMethodParameters(array $parameters): string 191 | { 192 | $params = ''; 193 | foreach ($parameters as $index => $parameter) { 194 | $params .= $this->renderMethodParameter($parameter) ; 195 | 196 | if ($index !== array_key_last($parameters)) { 197 | $params .= ', '; 198 | } 199 | } 200 | 201 | return $params; 202 | } 203 | 204 | /** 205 | * Renders a single parameter for a method. 206 | * 207 | * @param ParameterConfig $parameter Parameter config. 208 | * 209 | * @return string Method parameter as a string. 210 | */ 211 | private function renderMethodParameter(ParameterConfig $parameter): string 212 | { 213 | $type = $parameter->hasType() 214 | ? $this->renderType($parameter->type) 215 | : ''; 216 | $output = $type 217 | . ' $' 218 | . $parameter->name 219 | . $this->renderParameterDefaultValue($parameter); 220 | 221 | return ltrim($output); 222 | } 223 | 224 | /** 225 | * Renders default value for a parameter. Equal sign (surrounded with spaces) is included. 226 | * 227 | * @param ParameterConfig $parameter Parameter config. 228 | * 229 | * @return string Parameter's default value as a string. Empty string is returned when no default value was 230 | * specified. 231 | */ 232 | private function renderParameterDefaultValue(ParameterConfig $parameter): string 233 | { 234 | if (!$parameter->isDefaultValueAvailable) { 235 | return ''; 236 | } 237 | 238 | /** @var string $value */ 239 | $value = $parameter->isDefaultValueConstant 240 | ? $parameter->defaultValueConstantName 241 | : var_export($parameter->defaultValue, true); 242 | 243 | return ' = ' . $value; 244 | } 245 | 246 | /** 247 | * Renders a proxy method's body using {@see $proxyMethodBodyTemplate}. 248 | * 249 | * @param MethodConfig $method Method config. 250 | * 251 | * @return string Method body as a string. 252 | */ 253 | private function renderMethodBody(MethodConfig $method): string 254 | { 255 | $output = strtr($this->proxyMethodBodyTemplate, [ 256 | '{{return}}' => $this->renderIndent(2) . $this->renderReturn($method), 257 | '{{methodName}}' => "'" . $method->name . "'", 258 | '{{params}}' => $this->renderMethodCallParameters($method->parameters), 259 | ]); 260 | 261 | return "\n" . $output . "\n"; 262 | } 263 | 264 | /** 265 | * Renders return statement for a method. 266 | * 267 | * @param MethodConfig $method Method config. 268 | * 269 | * @return string Return statement as a string. Empty string is returned when no return type was specified or it was 270 | * explicitly specified as `void`. 271 | */ 272 | private function renderReturn(MethodConfig $method): string 273 | { 274 | if ($method->returnType?->name === 'void') { 275 | return ''; 276 | } 277 | 278 | return 'return '; 279 | } 280 | 281 | /** 282 | * Renders return type for a method. 283 | * 284 | * @param MethodConfig $method Method config. 285 | * 286 | * @return string Return type as a string. Empty string is returned when method has no return type. 287 | */ 288 | private function renderReturnType(MethodConfig $method): string 289 | { 290 | if (!$method->hasReturnType()) { 291 | return ''; 292 | } 293 | 294 | return ': ' . $this->renderType($method->returnType); 295 | } 296 | 297 | /** 298 | * Renders a type. Nullability is handled too. 299 | * 300 | * @param TypeConfig $type Type config. 301 | * 302 | * @return string Type as a string. 303 | */ 304 | private function renderType(TypeConfig $type): string 305 | { 306 | if ( 307 | $type->name === 'mixed' 308 | || !$type->allowsNull 309 | || str_contains($type->name, '|') 310 | ) { 311 | return $type->name; 312 | } 313 | 314 | return '?' . $type->name; 315 | } 316 | 317 | /** 318 | * Renders parameters passed to a proxy's method call. 319 | * 320 | * @param ParameterConfig[] $parameters A map where key is a {@see ParameterConfig::$name} and value is 321 | * {@see ParameterConfig} instance. 322 | * @psalm-param array $parameters 323 | * 324 | * @return string Parameters as a string. Empty string is returned when no parameters were passed. 325 | */ 326 | private function renderMethodCallParameters(array $parameters): string 327 | { 328 | $keys = array_keys($parameters); 329 | if ($keys === []) { 330 | return ''; 331 | } 332 | 333 | return '$' . implode(', $', $keys); 334 | } 335 | 336 | /** 337 | * Renders indent. 4 spaces are used, with no tabs. 338 | * 339 | * @param int $count How many times indent should be repeated. 340 | * 341 | * @return string Indent as a string. 342 | */ 343 | private function renderIndent(int $count = 1): string 344 | { 345 | return str_repeat(' ', $count); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/Config/ClassConfig.php: -------------------------------------------------------------------------------- 1 | 48 | */ 49 | public array $methods, 50 | ) { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Config/MethodConfig.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public array $parameters, 30 | /** 31 | * @var TypeConfig|null Return type config. `null` means no return type specified. 32 | */ 33 | public ?TypeConfig $returnType, 34 | ) { 35 | } 36 | 37 | /** 38 | * Whether a method has return type. 39 | * 40 | * @return bool `true` if return type specified and `false` otherwise. 41 | * 42 | * @psalm-assert-if-true TypeConfig $this->returnType 43 | */ 44 | public function hasReturnType(): bool 45 | { 46 | return $this->returnType !== null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Config/ParameterConfig.php: -------------------------------------------------------------------------------- 1 | type 59 | */ 60 | public function hasType(): bool 61 | { 62 | return $this->type !== null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Config/TypeConfig.php: -------------------------------------------------------------------------------- 1 | instance; 32 | } 33 | 34 | /** 35 | * Calls a method in the {@see $instance} additionally allowing to process result afterwards (even in case of 36 | * error). 37 | * 38 | * @param string $methodName A called method in the {@see $instance}. 39 | * @param array $arguments A list of arguments passed to a called method. The order must be maintained. 40 | * 41 | * @throws Throwable In case of error happen during the method call. 42 | * 43 | * @return $this|mixed Either a new instance of {@see $instance} class or return value of a called method. 44 | */ 45 | protected function call(string $methodName, array $arguments): mixed 46 | { 47 | $this->resetCurrentError(); 48 | $result = null; 49 | $timeStart = microtime(true); 50 | try { 51 | /** @var mixed $result */ 52 | $result = $this->callInternal($methodName, $arguments); 53 | } catch (Throwable $e) { 54 | $this->repeatError($e); 55 | } finally { 56 | /** @var mixed $result */ 57 | $result = $this->afterCall($methodName, $arguments, $result, $timeStart); 58 | } 59 | 60 | return $this->processResult($result); 61 | } 62 | 63 | /** 64 | * An event executed after each call of a method. Can be used for handling errors, logging, etc. `$result` must be 65 | * always returned. 66 | * 67 | * @param string $methodName A called method in the {@see $instance}. 68 | * @param array $arguments A list of arguments passed to a called method. The order must be maintained. 69 | * @param mixed $result Return value of a called method. 70 | * @param float $timeStart UNIX timestamp right before proxy method call. For example: `1656657586.4849`. 71 | * 72 | * @return mixed Return value of a called method. 73 | */ 74 | protected function afterCall( 75 | string $methodName, 76 | array $arguments, 77 | mixed $result, 78 | float $timeStart 79 | ): mixed { 80 | return $result; 81 | } 82 | 83 | /** 84 | * Gets new instance of {@see $instance} class. 85 | * 86 | * @param object $instance {@see $instance}. 87 | * 88 | * @return $this A new instance of the same class 89 | */ 90 | protected function getNewStaticInstance(object $instance): self 91 | { 92 | /** 93 | * @psalm-suppress UnsafeInstantiation Constructor should be consistent to `getNewStaticInstance()`. 94 | */ 95 | return new static($instance); 96 | } 97 | 98 | /** 99 | * Just calls a method in the {@see $instance}. 100 | * 101 | * @param string $methodName A called method in the {@see $instance}. 102 | * @param array $arguments A list of arguments passed to a called method. The order must be maintained. 103 | * 104 | * @return mixed Return value of a called method. 105 | */ 106 | private function callInternal(string $methodName, array $arguments): mixed 107 | { 108 | /** @psalm-suppress MixedMethodCall */ 109 | return $this->instance->$methodName(...$arguments); 110 | } 111 | 112 | /** 113 | * Processes return value of a called method - if it's an instance of the same class in {@see $instance} - a new 114 | * instance is created, otherwise it's returned as is. 115 | * 116 | * @param mixed $result Return value of a called method. 117 | * 118 | * @return $this|mixed Either a new instance of {@see $instance} class or return value of a called method. 119 | */ 120 | private function processResult(mixed $result): mixed 121 | { 122 | if (is_object($result) && get_class($result) === get_class($this->instance)) { 123 | $result = $this->getNewStaticInstance($result); 124 | } 125 | 126 | return $result; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/ProxyManager.php: -------------------------------------------------------------------------------- 1 | classCache = $cachePath ? new ClassCache($cachePath) : null; 36 | $this->classRenderer = new ClassRenderer(); 37 | $this->classConfigFactory = new ClassConfigFactory(); 38 | } 39 | 40 | /** 41 | * Creates object proxy based on an interface / a class and parent proxy class. 42 | * 43 | * @param string $baseStructure Either or an interface or a class for proxying method calls. 44 | * @param string $parentProxyClass A base proxy class which acts like a parent for dynamically created proxy. 45 | * {@see ObjectProxy} or a class extended from it must be used. 46 | * @param array $proxyConstructorArguments A list of arguments passed to proxy constructor 47 | * ({@see ObjectProxy::__construct}). 48 | * 49 | * @psalm-param class-string $baseStructure 50 | * 51 | * @throws Exception In case of error during creation or working with cache / requiring PHP code. 52 | * 53 | * @return ObjectProxy A subclass of {@see ObjectProxy}. 54 | */ 55 | public function createObjectProxy( 56 | string $baseStructure, 57 | string $parentProxyClass, 58 | array $proxyConstructorArguments 59 | ): ObjectProxy { 60 | $className = $baseStructure . self::PROXY_SUFFIX; 61 | /** @psalm-var class-string $shortClassName */ 62 | $shortClassName = self::getProxyClassName($className); 63 | 64 | if (class_exists($shortClassName)) { 65 | /** 66 | * @var ObjectProxy 67 | * @psalm-suppress MixedMethodCall 68 | */ 69 | return new $shortClassName(...$proxyConstructorArguments); 70 | } 71 | 72 | $classDeclaration = $this->classCache?->get($className, $parentProxyClass); 73 | if (!$classDeclaration) { 74 | $classConfig = $this->classConfigFactory->getClassConfig($baseStructure); 75 | $classConfig = $this->generateProxyClassConfig($classConfig, $parentProxyClass); 76 | $classDeclaration = $this->classRenderer->render($classConfig); 77 | $this->classCache?->set($baseStructure, $parentProxyClass, $classDeclaration); 78 | } 79 | if (!$this->classCache) { 80 | /** @psalm-suppress UnusedFunctionCall Bug https://github.com/vimeo/psalm/issues/8406 */ 81 | eval(str_replace('classCache->getClassPath($baseStructure, $parentProxyClass); 84 | /** @psalm-suppress UnresolvableInclude */ 85 | require $path; 86 | } 87 | 88 | /** 89 | * @var ObjectProxy 90 | * @psalm-suppress MixedMethodCall 91 | */ 92 | return new $shortClassName(...$proxyConstructorArguments); 93 | } 94 | 95 | /** 96 | * Generates class config for using with proxy from a regular class config. 97 | * 98 | * @param ClassConfig $classConfig Initial class config. 99 | * @param string $parentProxyClass A base proxy class which acts like a parent for dynamically created proxy. 100 | * {@see ObjectProxy} or a class extended from it must be used. 101 | * 102 | * @return ClassConfig Modified class config ready for using with proxy. 103 | */ 104 | private function generateProxyClassConfig(ClassConfig $classConfig, string $parentProxyClass): ClassConfig 105 | { 106 | if ($classConfig->isInterface) { 107 | $classConfig->isInterface = false; 108 | $classConfig->interfaces = [$classConfig->name]; 109 | } 110 | 111 | $classConfig->parent = $parentProxyClass; 112 | $classConfig->name .= self::PROXY_SUFFIX; 113 | $classConfig->shortName = self::getProxyClassName($classConfig->name); 114 | 115 | foreach ($classConfig->methods as $methodIndex => $method) { 116 | if ($method->name === '__construct') { 117 | unset($classConfig->methods[$methodIndex]); 118 | 119 | continue; 120 | } 121 | 122 | foreach ($method->modifiers as $index => $modifier) { 123 | if ($modifier === 'abstract') { 124 | unset($classConfig->methods[$methodIndex]->modifiers[$index]); 125 | } 126 | } 127 | } 128 | 129 | return $classConfig; 130 | } 131 | 132 | /** 133 | * Transforms full class / interface name with namespace to short class name for using in proxy. For example: 134 | * 135 | * - `Yiisoft\Proxy\Tests\Stub\GraphInterfaceProxy` becomes `Yiisoft_Proxy_Tests_Stub_GraphInterfaceProxy`. 136 | * - `Yiisoft\Proxy\Tests\Stub\GraphProxy` becomes `Yiisoft_Proxy_Tests_Stub_GraphProxy`. 137 | * 138 | * and so on. 139 | * 140 | * @param string $fullClassName Initial class name. 141 | * 142 | * @return string Proxy class name. 143 | */ 144 | private static function getProxyClassName(string $fullClassName): string 145 | { 146 | return str_replace('\\', '_', $fullClassName); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/ProxyTrait.php: -------------------------------------------------------------------------------- 1 | currentError; 31 | } 32 | 33 | /** 34 | * Whether a proxy has current error. 35 | * 36 | * @return bool `true` if it has current error and `false` otherwise. 37 | */ 38 | public function hasCurrentError(): bool 39 | { 40 | return $this->currentError !== null; 41 | } 42 | 43 | /** 44 | * Throws current error again. 45 | * 46 | * @param Throwable $error A throwable object. 47 | * 48 | * @throws Throwable An exact error previously stored in {@see $currentError}. 49 | */ 50 | protected function repeatError(Throwable $error): void 51 | { 52 | $this->currentError = $error; 53 | throw $error; 54 | } 55 | 56 | /** 57 | * Resets current error. 58 | */ 59 | protected function resetCurrentError(): void 60 | { 61 | $this->currentError = null; 62 | } 63 | } 64 | --------------------------------------------------------------------------------