├── .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 |
4 |
5 |
Yii Proxy
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/proxy)
10 | [](https://packagist.org/packages/yiisoft/proxy)
11 | [](https://github.com/yiisoft/proxy/actions?query=workflow%3Abuild)
12 | [](https://scrutinizer-ci.com/g/yiisoft/proxy/?branch=master)
13 | [](https://scrutinizer-ci.com/g/yiisoft/proxy/?branch=master)
14 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/proxy/master)
15 | [](https://github.com/yiisoft/proxy/actions?query=workflow%3A%22static+analysis%22)
16 | [](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 | [](https://opencollective.com/yiisoft)
151 |
152 | ## Follow updates
153 |
154 | [](https://www.yiiframework.com/)
155 | [](https://twitter.com/yiiframework)
156 | [](https://t.me/yii3en)
157 | [](https://www.facebook.com/groups/yiitalk)
158 | [](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 |
--------------------------------------------------------------------------------