summaryrefslogtreecommitdiff
path: root/vendor/symfony/var-exporter/ProxyHelper.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/symfony/var-exporter/ProxyHelper.php')
-rw-r--r--vendor/symfony/var-exporter/ProxyHelper.php413
1 files changed, 413 insertions, 0 deletions
diff --git a/vendor/symfony/var-exporter/ProxyHelper.php b/vendor/symfony/var-exporter/ProxyHelper.php
new file mode 100644
index 0000000..4cf0f65
--- /dev/null
+++ b/vendor/symfony/var-exporter/ProxyHelper.php
@@ -0,0 +1,413 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\VarExporter;
13
14use Symfony\Component\VarExporter\Exception\LogicException;
15use Symfony\Component\VarExporter\Internal\Hydrator;
16use Symfony\Component\VarExporter\Internal\LazyObjectRegistry;
17
18/**
19 * @author Nicolas Grekas <p@tchwork.com>
20 */
21final class ProxyHelper
22{
23 /**
24 * Helps generate lazy-loading ghost objects.
25 *
26 * @throws LogicException When the class is incompatible with ghost objects
27 */
28 public static function generateLazyGhost(\ReflectionClass $class): string
29 {
30 if (\PHP_VERSION_ID < 80300 && $class->isReadOnly()) {
31 throw new LogicException(sprintf('Cannot generate lazy ghost with PHP < 8.3: class "%s" is readonly.', $class->name));
32 }
33 if ($class->isFinal()) {
34 throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name));
35 }
36 if ($class->isInterface() || $class->isAbstract()) {
37 throw new LogicException(sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name));
38 }
39 if (\stdClass::class !== $class->name && $class->isInternal()) {
40 throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is internal.', $class->name));
41 }
42 if ($class->hasMethod('__get') && 'mixed' !== (self::exportType($class->getMethod('__get')) ?? 'mixed')) {
43 throw new LogicException(sprintf('Cannot generate lazy ghost: return type of method "%s::__get()" should be "mixed".', $class->name));
44 }
45
46 static $traitMethods;
47 $traitMethods ??= (new \ReflectionClass(LazyGhostTrait::class))->getMethods();
48
49 foreach ($traitMethods as $method) {
50 if ($class->hasMethod($method->name) && $class->getMethod($method->name)->isFinal()) {
51 throw new LogicException(sprintf('Cannot generate lazy ghost: method "%s::%s()" is final.', $class->name, $method->name));
52 }
53 }
54
55 $parent = $class;
56 while ($parent = $parent->getParentClass()) {
57 if (\stdClass::class !== $parent->name && $parent->isInternal()) {
58 throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.', $class->name, $parent->name));
59 }
60 }
61 $propertyScopes = self::exportPropertyScopes($class->name);
62
63 return <<<EOPHP
64 extends \\{$class->name} implements \Symfony\Component\VarExporter\LazyObjectInterface
65 {
66 use \Symfony\Component\VarExporter\LazyGhostTrait;
67
68 private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
69 }
70
71 // Help opcache.preload discover always-needed symbols
72 class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
73 class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
74 class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
75
76 EOPHP;
77 }
78
79 /**
80 * Helps generate lazy-loading virtual proxies.
81 *
82 * @param \ReflectionClass[] $interfaces
83 *
84 * @throws LogicException When the class is incompatible with virtual proxies
85 */
86 public static function generateLazyProxy(?\ReflectionClass $class, array $interfaces = []): string
87 {
88 if (!class_exists($class?->name ?? \stdClass::class, false)) {
89 throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not a class.', $class->name));
90 }
91 if ($class?->isFinal()) {
92 throw new LogicException(sprintf('Cannot generate lazy proxy: class "%s" is final.', $class->name));
93 }
94 if (\PHP_VERSION_ID < 80300 && $class?->isReadOnly()) {
95 throw new LogicException(sprintf('Cannot generate lazy proxy with PHP < 8.3: class "%s" is readonly.', $class->name));
96 }
97
98 $methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []];
99 foreach ($interfaces as $interface) {
100 if (!$interface->isInterface()) {
101 throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name));
102 }
103 $methodReflectors[] = $interface->getMethods();
104 }
105 $methodReflectors = array_merge(...$methodReflectors);
106
107 $extendsInternalClass = false;
108 if ($parent = $class) {
109 do {
110 $extendsInternalClass = \stdClass::class !== $parent->name && $parent->isInternal();
111 } while (!$extendsInternalClass && $parent = $parent->getParentClass());
112 }
113 $methodsHaveToBeProxied = $extendsInternalClass;
114 $methods = [];
115
116 foreach ($methodReflectors as $method) {
117 if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) {
118 continue;
119 }
120 $methodsHaveToBeProxied = true;
121 $trait = new \ReflectionMethod(LazyProxyTrait::class, '__get');
122 $body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine());
123 $body[0] = str_replace('): mixed', '): '.$type, $body[0]);
124 $methods['__get'] = strtr(implode('', $body).' }', [
125 'Hydrator' => '\\'.Hydrator::class,
126 'Registry' => '\\'.LazyObjectRegistry::class,
127 ]);
128 break;
129 }
130
131 foreach ($methodReflectors as $method) {
132 if (($method->isStatic() && !$method->isAbstract()) || isset($methods[$lcName = strtolower($method->name)])) {
133 continue;
134 }
135 if ($method->isFinal()) {
136 if ($extendsInternalClass || $methodsHaveToBeProxied || method_exists(LazyProxyTrait::class, $method->name)) {
137 throw new LogicException(sprintf('Cannot generate lazy proxy: method "%s::%s()" is final.', $class->name, $method->name));
138 }
139 continue;
140 }
141 if (method_exists(LazyProxyTrait::class, $method->name) || ($method->isProtected() && !$method->isAbstract())) {
142 continue;
143 }
144
145 $signature = self::exportSignature($method, true, $args);
146 $parentCall = $method->isAbstract() ? "throw new \BadMethodCallException('Cannot forward abstract method \"{$method->class}::{$method->name}()\".')" : "parent::{$method->name}({$args})";
147
148 if ($method->isStatic()) {
149 $body = " $parentCall;";
150 } elseif (str_ends_with($signature, '): never') || str_ends_with($signature, '): void')) {
151 $body = <<<EOPHP
152 if (isset(\$this->lazyObjectState)) {
153 (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}({$args});
154 } else {
155 {$parentCall};
156 }
157 EOPHP;
158 } else {
159 if (!$methodsHaveToBeProxied && !$method->isAbstract()) {
160 // Skip proxying methods that might return $this
161 foreach (preg_split('/[()|&]++/', self::exportType($method) ?? 'static') as $type) {
162 if (\in_array($type = ltrim($type, '?'), ['static', 'object'], true)) {
163 continue 2;
164 }
165 foreach ([$class, ...$interfaces] as $r) {
166 if ($r && is_a($r->name, $type, true)) {
167 continue 3;
168 }
169 }
170 }
171 }
172
173 $body = <<<EOPHP
174 if (isset(\$this->lazyObjectState)) {
175 return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}({$args});
176 }
177
178 return {$parentCall};
179 EOPHP;
180 }
181 $methods[$lcName] = " {$signature}\n {\n{$body}\n }";
182 }
183
184 $types = $interfaces = array_unique(array_column($interfaces, 'name'));
185 $interfaces[] = LazyObjectInterface::class;
186 $interfaces = implode(', \\', $interfaces);
187 $parent = $class ? ' extends \\'.$class->name : '';
188 array_unshift($types, $class ? 'parent' : '');
189 $type = ltrim(implode('&\\', $types), '&');
190
191 if (!$class) {
192 $trait = new \ReflectionMethod(LazyProxyTrait::class, 'initializeLazyObject');
193 $body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine());
194 $body[0] = str_replace('): parent', '): '.$type, $body[0]);
195 $methods = ['initializeLazyObject' => implode('', $body).' }'] + $methods;
196 }
197 $body = $methods ? "\n".implode("\n\n", $methods)."\n" : '';
198 $propertyScopes = $class ? self::exportPropertyScopes($class->name) : '[]';
199
200 if (
201 $class?->hasMethod('__unserialize')
202 && !$class->getMethod('__unserialize')->getParameters()[0]->getType()
203 ) {
204 // fix contravariance type problem when $class declares a `__unserialize()` method without typehint.
205 $lazyProxyTraitStatement = <<<EOPHP
206 use \Symfony\Component\VarExporter\LazyProxyTrait {
207 __unserialize as private __doUnserialize;
208 }
209 EOPHP;
210
211 $body .= <<<EOPHP
212
213 public function __unserialize(\$data): void
214 {
215 \$this->__doUnserialize(\$data);
216 }
217
218 EOPHP;
219 } else {
220 $lazyProxyTraitStatement = <<<EOPHP
221 use \Symfony\Component\VarExporter\LazyProxyTrait;
222 EOPHP;
223 }
224
225 return <<<EOPHP
226 {$parent} implements \\{$interfaces}
227 {
228 {$lazyProxyTraitStatement}
229
230 private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
231 {$body}}
232
233 // Help opcache.preload discover always-needed symbols
234 class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
235 class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
236 class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
237
238 EOPHP;
239 }
240
241 public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
242 {
243 $byRefIndex = 0;
244 $args = '';
245 $param = null;
246 $parameters = [];
247 $namespace = $function instanceof \ReflectionMethod ? $function->class : $function->getNamespaceName().'\\';
248 $namespace = substr($namespace, 0, strrpos($namespace, '\\') ?: 0);
249 foreach ($function->getParameters() as $param) {
250 $parameters[] = ($param->getAttributes(\SensitiveParameter::class) ? '#[\SensitiveParameter] ' : '')
251 .($withParameterTypes && $param->hasType() ? self::exportType($param).' ' : '')
252 .($param->isPassedByReference() ? '&' : '')
253 .($param->isVariadic() ? '...' : '').'$'.$param->name
254 .($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param, $namespace) : '');
255 if ($param->isPassedByReference()) {
256 $byRefIndex = 1 + $param->getPosition();
257 }
258 $args .= ($param->isVariadic() ? '...$' : '$').$param->name.', ';
259 }
260
261 if (!$param || !$byRefIndex) {
262 $args = '...\func_get_args()';
263 } elseif ($param->isVariadic()) {
264 $args = substr($args, 0, -2);
265 } else {
266 $args = explode(', ', $args, 1 + $byRefIndex);
267 $args[$byRefIndex] = sprintf('...\array_slice(\func_get_args(), %d)', $byRefIndex);
268 $args = implode(', ', $args);
269 }
270
271 $signature = 'function '.($function->returnsReference() ? '&' : '')
272 .($function->isClosure() ? '' : $function->name).'('.implode(', ', $parameters).')';
273
274 if ($function instanceof \ReflectionMethod) {
275 $signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' : 'private '))
276 .($function->isStatic() ? 'static ' : '').$signature;
277 }
278 if ($function->hasReturnType()) {
279 $signature .= ': '.self::exportType($function);
280 }
281
282 static $getPrototype;
283 $getPrototype ??= (new \ReflectionMethod(\ReflectionMethod::class, 'getPrototype'))->invoke(...);
284
285 while ($function) {
286 if ($function->hasTentativeReturnType()) {
287 return '#[\ReturnTypeWillChange] '.$signature;
288 }
289
290 try {
291 $function = $function instanceof \ReflectionMethod && $function->isAbstract() ? false : $getPrototype($function);
292 } catch (\ReflectionException) {
293 break;
294 }
295 }
296
297 return $signature;
298 }
299
300 public static function exportType(\ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionParameter $owner, bool $noBuiltin = false, ?\ReflectionType $type = null): ?string
301 {
302 if (!$type ??= $owner instanceof \ReflectionFunctionAbstract ? $owner->getReturnType() : $owner->getType()) {
303 return null;
304 }
305 $class = null;
306 $types = [];
307 if ($type instanceof \ReflectionUnionType) {
308 $reflectionTypes = $type->getTypes();
309 $glue = '|';
310 } elseif ($type instanceof \ReflectionIntersectionType) {
311 $reflectionTypes = $type->getTypes();
312 $glue = '&';
313 } else {
314 $reflectionTypes = [$type];
315 $glue = null;
316 }
317
318 foreach ($reflectionTypes as $type) {
319 if ($type instanceof \ReflectionIntersectionType) {
320 if ('' !== $name = '('.self::exportType($owner, $noBuiltin, $type).')') {
321 $types[] = $name;
322 }
323 continue;
324 }
325 $name = $type->getName();
326
327 if ($noBuiltin && $type->isBuiltin()) {
328 continue;
329 }
330 if (\in_array($name, ['parent', 'self'], true) && $class ??= $owner->getDeclaringClass()) {
331 $name = 'parent' === $name ? ($class->getParentClass() ?: null)?->name ?? 'parent' : $class->name;
332 }
333
334 $types[] = ($noBuiltin || $type->isBuiltin() || 'static' === $name ? '' : '\\').$name;
335 }
336
337 if (!$types) {
338 return '';
339 }
340 if (null === $glue) {
341 return (!$noBuiltin && $type->allowsNull() && !\in_array($name, ['mixed', 'null'], true) ? '?' : '').$types[0];
342 }
343 sort($types);
344
345 return implode($glue, $types);
346 }
347
348 private static function exportPropertyScopes(string $parent): string
349 {
350 $propertyScopes = Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent);
351 uksort($propertyScopes, 'strnatcmp');
352 foreach ($propertyScopes as $k => $v) {
353 unset($propertyScopes[$k][3]);
354 }
355 $propertyScopes = VarExporter::export($propertyScopes);
356 $propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes);
357 $propertyScopes = preg_replace("/(?|(,)\n( ) |\n |,\n (\]))/", '$1$2', $propertyScopes);
358 $propertyScopes = str_replace("\n", "\n ", $propertyScopes);
359
360 return $propertyScopes;
361 }
362
363 private static function exportDefault(\ReflectionParameter $param, $namespace): string
364 {
365 $default = rtrim(substr(explode('$'.$param->name.' = ', (string) $param, 2)[1] ?? '', 0, -2));
366
367 if (\in_array($default, ['<default>', 'NULL'], true)) {
368 return 'null';
369 }
370 if (str_ends_with($default, "...'") && preg_match("/^'(?:[^'\\\\]*+(?:\\\\.)*+)*+'$/", $default)) {
371 return VarExporter::export($param->getDefaultValue());
372 }
373
374 $regexp = "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/";
375 $parts = preg_split($regexp, $default, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
376
377 $regexp = '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(\(?)(?!: )/';
378 $callback = (false !== strpbrk($default, "\\:('") && $class = $param->getDeclaringClass())
379 ? fn ($m) => $m[1].match ($m[2]) {
380 'new', 'false', 'true', 'null' => $m[2],
381 'NULL' => 'null',
382 'self' => '\\'.$class->name,
383 'namespace\\parent',
384 'parent' => ($parent = $class->getParentClass()) ? '\\'.$parent->name : 'parent',
385 default => self::exportSymbol($m[2], '(' !== $m[3], $namespace),
386 }.$m[3]
387 : fn ($m) => $m[1].match ($m[2]) {
388 'new', 'false', 'true', 'null', 'self', 'parent' => $m[2],
389 'NULL' => 'null',
390 default => self::exportSymbol($m[2], '(' !== $m[3], $namespace),
391 }.$m[3];
392
393 return implode('', array_map(fn ($part) => match ($part[0]) {
394 '"' => $part, // for internal classes only
395 "'" => false !== strpbrk($part, "\\\0\r\n") ? '"'.substr(str_replace(['$', "\0", "\r", "\n"], ['\$', '\0', '\r', '\n'], $part), 1, -1).'"' : $part,
396 default => preg_replace_callback($regexp, $callback, $part),
397 }, $parts));
398 }
399
400 private static function exportSymbol(string $symbol, bool $mightBeRootConst, string $namespace): string
401 {
402 if (!$mightBeRootConst
403 || false === ($ns = strrpos($symbol, '\\'))
404 || substr($symbol, 0, $ns) !== $namespace
405 || \defined($symbol)
406 || !\defined(substr($symbol, $ns + 1))
407 ) {
408 return '\\'.$symbol;
409 }
410
411 return '\\'.substr($symbol, $ns + 1);
412 }
413}