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

  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($methodtrue$args);
  129.             $parentCall $method->isAbstract() ? "throw new \BadMethodCallException('Cannot forward abstract method \"{$method->class}::{$method->name}()\".')" "parent::{$method->name}({$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}({$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}({$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 truestring &$args null): string
  190.     {
  191.         $hasByRef false;
  192.         $args '';
  193.         $param null;
  194.         $parameters = [];
  195.         foreach ($function->getParameters() as $param) {
  196.             $parameters[] = ($param->getAttributes(\SensitiveParameter::class) ? '#[\SensitiveParameter] ' '')
  197.                 .($withParameterTypes && $param->hasType() ? self::exportType($param).' ' '')
  198.                 .($param->isPassedByReference() ? '&' '')
  199.                 .($param->isVariadic() ? '...' '').'$'.$param->name
  200.                 .($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param) : '');
  201.             $hasByRef $hasByRef || $param->isPassedByReference();
  202.             $args .= ($param->isVariadic() ? '...$' '$').$param->name.', ';
  203.         }
  204.         if (!$param || !$hasByRef) {
  205.             $args '...\func_get_args()';
  206.         } elseif ($param->isVariadic()) {
  207.             $args substr($args0, -2);
  208.         } else {
  209.             $args .= sprintf('...\array_slice(\func_get_args(), %d)'\count($parameters));
  210.         }
  211.         $signature 'function '.($function->returnsReference() ? '&' '')
  212.             .($function->isClosure() ? '' $function->name).'('.implode(', '$parameters).')';
  213.         if ($function instanceof \ReflectionMethod) {
  214.             $signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' 'private '))
  215.                 .($function->isStatic() ? 'static ' '').$signature;
  216.         }
  217.         if ($function->hasReturnType()) {
  218.             $signature .= ': '.self::exportType($function);
  219.         }
  220.         static $getPrototype;
  221.         $getPrototype ??= (new \ReflectionMethod(\ReflectionMethod::class, 'getPrototype'))->invoke(...);
  222.         while ($function) {
  223.             if ($function->hasTentativeReturnType()) {
  224.                 return '#[\ReturnTypeWillChange] '.$signature;
  225.             }
  226.             try {
  227.                 $function $function instanceof \ReflectionMethod && $function->isAbstract() ? false $getPrototype($function);
  228.             } catch (\ReflectionException) {
  229.                 break;
  230.             }
  231.         }
  232.         return $signature;
  233.     }
  234.     public static function exportType(\ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionParameter $ownerbool $noBuiltin false\ReflectionType $type null): ?string
  235.     {
  236.         if (!$type ??= $owner instanceof \ReflectionFunctionAbstract $owner->getReturnType() : $owner->getType()) {
  237.             return null;
  238.         }
  239.         $class null;
  240.         $types = [];
  241.         if ($type instanceof \ReflectionUnionType) {
  242.             $reflectionTypes $type->getTypes();
  243.             $glue '|';
  244.         } elseif ($type instanceof \ReflectionIntersectionType) {
  245.             $reflectionTypes $type->getTypes();
  246.             $glue '&';
  247.         } else {
  248.             $reflectionTypes = [$type];
  249.             $glue null;
  250.         }
  251.         foreach ($reflectionTypes as $type) {
  252.             if ($type instanceof \ReflectionIntersectionType) {
  253.                 if ('' !== $name '('.self::exportType($owner$noBuiltin$type).')') {
  254.                     $types[] = $name;
  255.                 }
  256.                 continue;
  257.             }
  258.             $name $type->getName();
  259.             if ($noBuiltin && $type->isBuiltin()) {
  260.                 continue;
  261.             }
  262.             if (\in_array($name, ['parent''self'], true) && $class ??= $owner->getDeclaringClass()) {
  263.                 $name 'parent' === $name ? ($class->getParentClass() ?: null)?->name ?? 'parent' $class->name;
  264.             }
  265.             $types[] = ($noBuiltin || $type->isBuiltin() || 'static' === $name '' '\\').$name;
  266.         }
  267.         if (!$types) {
  268.             return '';
  269.         }
  270.         if (null === $glue) {
  271.             return (!$noBuiltin && $type->allowsNull() && 'mixed' !== $name '?' '').$types[0];
  272.         }
  273.         sort($types);
  274.         return implode($glue$types);
  275.     }
  276.     private static function exportPropertyScopes(string $parent): string
  277.     {
  278.         $propertyScopes Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent);
  279.         uksort($propertyScopes'strnatcmp');
  280.         $propertyScopes VarExporter::export($propertyScopes);
  281.         $propertyScopes str_replace(VarExporter::export($parent), 'parent::class'$propertyScopes);
  282.         $propertyScopes preg_replace("/(?|(,)\n( )       |\n        |,\n    (\]))/"'$1$2'$propertyScopes);
  283.         $propertyScopes str_replace("\n""\n    "$propertyScopes);
  284.         return $propertyScopes;
  285.     }
  286.     private static function exportDefault(\ReflectionParameter $param): string
  287.     {
  288.         $default rtrim(substr(explode('$'.$param->name.' = ', (string) $param2)[1] ?? ''0, -2));
  289.         if (\in_array($default, ['<default>''NULL'], true)) {
  290.             return 'null';
  291.         }
  292.         if (str_ends_with($default"...'") && preg_match("/^'(?:[^'\\\\]*+(?:\\\\.)*+)*+'$/"$default)) {
  293.             return VarExporter::export($param->getDefaultValue());
  294.         }
  295.         $regexp "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/";
  296.         $parts preg_split($regexp$default, -1\PREG_SPLIT_DELIM_CAPTURE \PREG_SPLIT_NO_EMPTY);
  297.         $regexp '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(?!: )/';
  298.         $callback = (false !== strpbrk($default"\\:('") && $class $param->getDeclaringClass())
  299.             ? fn ($m) => $m[1].match ($m[2]) {
  300.                 'new''false''true''null' => $m[2],
  301.                 'NULL' => 'null',
  302.                 'self' => '\\'.$class->name,
  303.                 'namespace\\parent',
  304.                 'parent' => ($parent $class->getParentClass()) ? '\\'.$parent->name 'parent',
  305.                 default => '\\'.$m[2],
  306.             }
  307.             : fn ($m) => $m[1].match ($m[2]) {
  308.                 'new''false''true''null''self''parent' => $m[2],
  309.                 'NULL' => 'null',
  310.                 default => '\\'.$m[2],
  311.             };
  312.         return implode(''array_map(fn ($part) => match ($part[0]) {
  313.             '"' => $part// for internal classes only
  314.             "'" => false !== strpbrk($part"\\\0\r\n") ? '"'.substr(str_replace(['$'"\0""\r""\n"], ['\$''\0''\r''\n'], $part), 1, -1).'"' $part,
  315.             default => preg_replace_callback($regexp$callback$part),
  316.         }, $parts));
  317.     }
  318. }