vendor/symfony/var-exporter/ProxyHelper.php line 256

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\VarExporter;
  11. use Symfony\Component\VarExporter\Exception\LogicException;
  12. use Symfony\Component\VarExporter\Internal\Hydrator;
  13. use Symfony\Component\VarExporter\Internal\LazyObjectRegistry;
  14. /**
  15.  * @author Nicolas Grekas <p@tchwork.com>
  16.  */
  17. final class ProxyHelper
  18. {
  19.     /**
  20.      * Helps generate lazy-loading ghost objects.
  21.      *
  22.      * @throws LogicException When the class is incompatible with ghost objects
  23.      */
  24.     public static function generateLazyGhost(\ReflectionClass $class): string
  25.     {
  26.         if (\PHP_VERSION_ID >= 80200 && \PHP_VERSION_ID 80300 && $class->isReadOnly()) {
  27.             throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is readonly.'$class->name));
  28.         }
  29.         if ($class->isFinal()) {
  30.             throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is final.'$class->name));
  31.         }
  32.         if ($class->isInterface() || $class->isAbstract()) {
  33.             throw new LogicException(sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.'$class->name));
  34.         }
  35.         if (\stdClass::class !== $class->name && $class->isInternal()) {
  36.             throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is internal.'$class->name));
  37.         }
  38.         if ($class->hasMethod('__get') && 'mixed' !== (self::exportType($class->getMethod('__get')) ?? 'mixed')) {
  39.             throw new LogicException(sprintf('Cannot generate lazy ghost: return type of method "%s::__get()" should be "mixed".'$class->name));
  40.         }
  41.         static $traitMethods;
  42.         $traitMethods ??= (new \ReflectionClass(LazyGhostTrait::class))->getMethods();
  43.         foreach ($traitMethods as $method) {
  44.             if ($class->hasMethod($method->name) && $class->getMethod($method->name)->isFinal()) {
  45.                 throw new LogicException(sprintf('Cannot generate lazy ghost: method "%s::%s()" is final.'$class->name$method->name));
  46.             }
  47.         }
  48.         $parent $class;
  49.         while ($parent $parent->getParentClass()) {
  50.             if (\stdClass::class !== $parent->name && $parent->isInternal()) {
  51.                 throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.'$class->name$parent->name));
  52.             }
  53.         }
  54.         $propertyScopes self::exportPropertyScopes($class->name);
  55.         return <<<EOPHP
  56.              extends \\{$class->name} implements \Symfony\Component\VarExporter\LazyObjectInterface
  57.             {
  58.                 use \Symfony\Component\VarExporter\LazyGhostTrait;
  59.                 private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
  60.             }
  61.             // Help opcache.preload discover always-needed symbols
  62.             class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
  63.             class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
  64.             class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
  65.             EOPHP;
  66.     }
  67.     /**
  68.      * Helps generate lazy-loading virtual proxies.
  69.      *
  70.      * @param \ReflectionClass[] $interfaces
  71.      *
  72.      * @throws LogicException When the class is incompatible with virtual proxies
  73.      */
  74.     public static function generateLazyProxy(?\ReflectionClass $class, array $interfaces = []): string
  75.     {
  76.         if (!class_exists($class?->name ?? \stdClass::class, false)) {
  77.             throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not a class.'$class->name));
  78.         }
  79.         if ($class?->isFinal()) {
  80.             throw new LogicException(sprintf('Cannot generate lazy proxy: class "%s" is final.'$class->name));
  81.         }
  82.         if (\PHP_VERSION_ID >= 80200 && \PHP_VERSION_ID 80300 && $class?->isReadOnly()) {
  83.             throw new LogicException(sprintf('Cannot generate lazy proxy: class "%s" is readonly.'$class->name));
  84.         }
  85.         $methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC \ReflectionMethod::IS_PROTECTED) ?? []];
  86.         foreach ($interfaces as $interface) {
  87.             if (!$interface->isInterface()) {
  88.                 throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not an interface.'$interface->name));
  89.             }
  90.             $methodReflectors[] = $interface->getMethods();
  91.         }
  92.         $methodReflectors array_merge(...$methodReflectors);
  93.         $extendsInternalClass false;
  94.         if ($parent $class) {
  95.             do {
  96.                 $extendsInternalClass \stdClass::class !== $parent->name && $parent->isInternal();
  97.             } while (!$extendsInternalClass && $parent $parent->getParentClass());
  98.         }
  99.         $methodsHaveToBeProxied $extendsInternalClass;
  100.         $methods = [];
  101.         foreach ($methodReflectors as $method) {
  102.             if ('__get' !== strtolower($method->name) || 'mixed' === ($type self::exportType($method) ?? 'mixed')) {
  103.                 continue;
  104.             }
  105.             $methodsHaveToBeProxied true;
  106.             $trait = new \ReflectionMethod(LazyProxyTrait::class, '__get');
  107.             $body \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1$trait->getEndLine() - $trait->getStartLine());
  108.             $body[0] = str_replace('): mixed''): '.$type$body[0]);
  109.             $methods['__get'] = strtr(implode(''$body).'    }', [
  110.                 'Hydrator' => '\\'.Hydrator::class,
  111.                 'Registry' => '\\'.LazyObjectRegistry::class,
  112.             ]);
  113.             break;
  114.         }
  115.         foreach ($methodReflectors as $method) {
  116.             if (($method->isStatic() && !$method->isAbstract()) || isset($methods[$lcName strtolower($method->name)])) {
  117.                 continue;
  118.             }
  119.             if ($method->isFinal()) {
  120.                 if ($extendsInternalClass || $methodsHaveToBeProxied || method_exists(LazyProxyTrait::class, $method->name)) {
  121.                     throw new LogicException(sprintf('Cannot generate lazy proxy: method "%s::%s()" is final.'$class->name$method->name));
  122.                 }
  123.                 continue;
  124.             }
  125.             if (method_exists(LazyProxyTrait::class, $method->name) || ($method->isProtected() && !$method->isAbstract())) {
  126.                 continue;
  127.             }
  128.             $signature self::exportSignature($method);
  129.             $parentCall $method->isAbstract() ? "throw new \BadMethodCallException('Cannot forward abstract method \"{$method->class}::{$method->name}()\".')" "parent::{$method->name}(...\\func_get_args())";
  130.             if ($method->isStatic()) {
  131.                 $body "        $parentCall;";
  132.             } elseif (str_ends_with($signature'): never') || str_ends_with($signature'): void')) {
  133.                 $body = <<<EOPHP
  134.                         if (isset(\$this->lazyObjectState)) {
  135.                             (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}(...\\func_get_args());
  136.                         } else {
  137.                             {$parentCall};
  138.                         }
  139.                 EOPHP;
  140.             } else {
  141.                 if (!$methodsHaveToBeProxied && !$method->isAbstract()) {
  142.                     // Skip proxying methods that might return $this
  143.                     foreach (preg_split('/[()|&]++/'self::exportType($method) ?? 'static') as $type) {
  144.                         if (\in_array($type ltrim($type'?'), ['static''object'], true)) {
  145.                             continue 2;
  146.                         }
  147.                         foreach ([$class, ...$interfaces] as $r) {
  148.                             if ($r && is_a($r->name$typetrue)) {
  149.                                 continue 3;
  150.                             }
  151.                         }
  152.                     }
  153.                 }
  154.                 $body = <<<EOPHP
  155.                         if (isset(\$this->lazyObjectState)) {
  156.                             return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}(...\\func_get_args());
  157.                         }
  158.                         return {$parentCall};
  159.                 EOPHP;
  160.             }
  161.             $methods[$lcName] = "    {$signature}\n    {\n{$body}\n    }";
  162.         }
  163.         $types $interfaces array_unique(array_column($interfaces'name'));
  164.         $interfaces[] = LazyObjectInterface::class;
  165.         $interfaces implode(', \\'$interfaces);
  166.         $parent $class ' extends \\'.$class->name '';
  167.         array_unshift($types$class 'parent' '');
  168.         $type ltrim(implode('&\\'$types), '&');
  169.         if (!$class) {
  170.             $trait = new \ReflectionMethod(LazyProxyTrait::class, 'initializeLazyObject');
  171.             $body \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1$trait->getEndLine() - $trait->getStartLine());
  172.             $body[0] = str_replace('): parent''): '.$type$body[0]);
  173.             $methods = ['initializeLazyObject' => implode(''$body).'    }'] + $methods;
  174.         }
  175.         $body $methods "\n".implode("\n\n"$methods)."\n" '';
  176.         $propertyScopes $class self::exportPropertyScopes($class->name) : '[]';
  177.         return <<<EOPHP
  178.             {$parent} implements \\{$interfaces}
  179.             {
  180.                 use \Symfony\Component\VarExporter\LazyProxyTrait;
  181.                 private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
  182.             {$body}}
  183.             // Help opcache.preload discover always-needed symbols
  184.             class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
  185.             class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
  186.             class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
  187.             EOPHP;
  188.     }
  189.     public static function exportSignature(\ReflectionFunctionAbstract $functionbool $withParameterTypes true): string
  190.     {
  191.         $parameters = [];
  192.         foreach ($function->getParameters() as $param) {
  193.             $parameters[] = ($param->getAttributes(\SensitiveParameter::class) ? '#[\SensitiveParameter] ' '')
  194.                 .($withParameterTypes && $param->hasType() ? self::exportType($param).' ' '')
  195.                 .($param->isPassedByReference() ? '&' '')
  196.                 .($param->isVariadic() ? '...' '').'$'.$param->name
  197.                 .($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param) : '');
  198.         }
  199.         $signature 'function '.($function->returnsReference() ? '&' '')
  200.             .($function->isClosure() ? '' $function->name).'('.implode(', '$parameters).')';
  201.         if ($function instanceof \ReflectionMethod) {
  202.             $signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' 'private '))
  203.                 .($function->isStatic() ? 'static ' '').$signature;
  204.         }
  205.         if ($function->hasReturnType()) {
  206.             $signature .= ': '.self::exportType($function);
  207.         }
  208.         static $getPrototype;
  209.         $getPrototype ??= (new \ReflectionMethod(\ReflectionMethod::class, 'getPrototype'))->invoke(...);
  210.         while ($function) {
  211.             if ($function->hasTentativeReturnType()) {
  212.                 return '#[\ReturnTypeWillChange] '.$signature;
  213.             }
  214.             try {
  215.                 $function $function instanceof \ReflectionMethod && $function->isAbstract() ? false $getPrototype($function);
  216.             } catch (\ReflectionException) {
  217.                 break;
  218.             }
  219.         }
  220.         return $signature;
  221.     }
  222.     public static function exportType(\ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionParameter $ownerbool $noBuiltin false\ReflectionType $type null): ?string
  223.     {
  224.         if (!$type ??= $owner instanceof \ReflectionFunctionAbstract $owner->getReturnType() : $owner->getType()) {
  225.             return null;
  226.         }
  227.         $class null;
  228.         $types = [];
  229.         if ($type instanceof \ReflectionUnionType) {
  230.             $reflectionTypes $type->getTypes();
  231.             $glue '|';
  232.         } elseif ($type instanceof \ReflectionIntersectionType) {
  233.             $reflectionTypes $type->getTypes();
  234.             $glue '&';
  235.         } else {
  236.             $reflectionTypes = [$type];
  237.             $glue null;
  238.         }
  239.         foreach ($reflectionTypes as $type) {
  240.             if ($type instanceof \ReflectionIntersectionType) {
  241.                 if ('' !== $name '('.self::exportType($owner$noBuiltin$type).')') {
  242.                     $types[] = $name;
  243.                 }
  244.                 continue;
  245.             }
  246.             $name $type->getName();
  247.             if ($noBuiltin && $type->isBuiltin()) {
  248.                 continue;
  249.             }
  250.             if (\in_array($name, ['parent''self'], true) && $class ??= $owner->getDeclaringClass()) {
  251.                 $name 'parent' === $name ? ($class->getParentClass() ?: null)?->name ?? 'parent' $class->name;
  252.             }
  253.             $types[] = ($noBuiltin || $type->isBuiltin() || 'static' === $name '' '\\').$name;
  254.         }
  255.         if (!$types) {
  256.             return '';
  257.         }
  258.         if (null === $glue) {
  259.             return (!$noBuiltin && $type->allowsNull() && 'mixed' !== $name '?' '').$types[0];
  260.         }
  261.         sort($types);
  262.         return implode($glue$types);
  263.     }
  264.     private static function exportPropertyScopes(string $parent): string
  265.     {
  266.         $propertyScopes Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent);
  267.         uksort($propertyScopes'strnatcmp');
  268.         $propertyScopes VarExporter::export($propertyScopes);
  269.         $propertyScopes str_replace(VarExporter::export($parent), 'parent::class'$propertyScopes);
  270.         $propertyScopes preg_replace("/(?|(,)\n( )       |\n        |,\n    (\]))/"'$1$2'$propertyScopes);
  271.         $propertyScopes str_replace("\n""\n    "$propertyScopes);
  272.         return $propertyScopes;
  273.     }
  274.     private static function exportDefault(\ReflectionParameter $param): string
  275.     {
  276.         $default rtrim(substr(explode('$'.$param->name.' = ', (string) $param2)[1] ?? ''0, -2));
  277.         if (\in_array($default, ['<default>''NULL'], true)) {
  278.             return 'null';
  279.         }
  280.         if (str_ends_with($default"...'") && preg_match("/^'(?:[^'\\\\]*+(?:\\\\.)*+)*+'$/"$default)) {
  281.             return VarExporter::export($param->getDefaultValue());
  282.         }
  283.         $regexp "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/";
  284.         $parts preg_split($regexp$default, -1\PREG_SPLIT_DELIM_CAPTURE \PREG_SPLIT_NO_EMPTY);
  285.         $regexp '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(?!: )/';
  286.         $callback = (false !== strpbrk($default"\\:('") && $class $param->getDeclaringClass())
  287.             ? fn ($m) => $m[1].match ($m[2]) {
  288.                 'new''false''true''null' => $m[2],
  289.                 'NULL' => 'null',
  290.                 'self' => '\\'.$class->name,
  291.                 'namespace\\parent',
  292.                 'parent' => ($parent $class->getParentClass()) ? '\\'.$parent->name 'parent',
  293.                 default => '\\'.$m[2],
  294.             }
  295.             : fn ($m) => $m[1].match ($m[2]) {
  296.                 'new''false''true''null''self''parent' => $m[2],
  297.                 'NULL' => 'null',
  298.                 default => '\\'.$m[2],
  299.             };
  300.         return implode(''array_map(fn ($part) => match ($part[0]) {
  301.             '"' => $part// for internal classes only
  302.             "'" => false !== strpbrk($part"\\\0\r\n") ? '"'.substr(str_replace(['$'"\0""\r""\n"], ['\$''\0''\r''\n'], $part), 1, -1).'"' $part,
  303.             default => preg_replace_callback($regexp$callback$part),
  304.         }, $parts));
  305.     }
  306. }