diff --git a/.github/expected-missing-return-types.diff b/.github/expected-missing-return-types.diff index a59d20fd2811e..6940232ce0f04 100644 --- a/.github/expected-missing-return-types.diff +++ b/.github/expected-missing-return-types.diff @@ -7,10 +7,10 @@ head=$(sed '/^diff /Q' .github/expected-missing-return-types.diff) git checkout composer.json src/ diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php -index 165797504b..0c0922088a 100644 +index 18b5c21b9f..8fca8244e3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php -@@ -87,5 +87,5 @@ abstract class KernelTestCase extends TestCase +@@ -88,5 +88,5 @@ abstract class KernelTestCase extends TestCase * @return Container */ - protected static function getContainer(): ContainerInterface @@ -156,52 +156,52 @@ index 6b1c6c5fbe..bb80ed461e 100644 + public function isFresh(ResourceInterface $resource, int $timestamp): bool; } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php -index 64068fcc23..f29aaf1b94 100644 +index 53f2021fa9..cf95c1fe99 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php -@@ -218,5 +218,5 @@ class Application implements ResetInterface +@@ -219,5 +219,5 @@ class Application implements ResetInterface * @return int 0 if everything went fine, or an error code */ - public function doRun(InputInterface $input, OutputInterface $output) + public function doRun(InputInterface $input, OutputInterface $output): int { if (true === $input->hasParameterOption(['--version', '-V'], true)) { -@@ -454,5 +454,5 @@ class Application implements ResetInterface +@@ -453,5 +453,5 @@ class Application implements ResetInterface * @return string */ - public function getLongVersion() + public function getLongVersion(): string { if ('UNKNOWN' !== $this->getName()) { -@@ -497,5 +497,5 @@ class Application implements ResetInterface +@@ -496,5 +496,5 @@ class Application implements ResetInterface * @return Command|null */ - public function add(Command $command) + public function add(Command $command): ?Command { $this->init(); -@@ -534,5 +534,5 @@ class Application implements ResetInterface +@@ -533,5 +533,5 @@ class Application implements ResetInterface * @throws CommandNotFoundException When given command name does not exist */ - public function get(string $name) + public function get(string $name): Command { $this->init(); -@@ -641,5 +641,5 @@ class Application implements ResetInterface +@@ -640,5 +640,5 @@ class Application implements ResetInterface * @throws CommandNotFoundException When command name is incorrect or ambiguous */ - public function find(string $name) + public function find(string $name): Command { $this->init(); -@@ -751,5 +751,5 @@ class Application implements ResetInterface +@@ -750,5 +750,5 @@ class Application implements ResetInterface * @return Command[] */ - public function all(string $namespace = null) + public function all(string $namespace = null): array { $this->init(); -@@ -950,5 +950,5 @@ class Application implements ResetInterface +@@ -949,5 +949,5 @@ class Application implements ResetInterface * @return int 0 if everything went fine, or an error code */ - protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) @@ -234,7 +234,7 @@ index b41e691537..34de10fa70 100644 { if (null === $this->helperSet) { diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatter.php b/src/Symfony/Component/Console/Formatter/OutputFormatter.php -index 3c6b0efccd..121664f15a 100644 +index 0112350a50..dc972564fb 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatter.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatter.php @@ -137,5 +137,5 @@ class OutputFormatter implements WrappableOutputFormatterInterface @@ -301,22 +301,22 @@ index 2f1631ed30..a4b572771e 100644 { if (\is_array($value)) { diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php -index 04b7022484..5d736ec754 100644 +index 20ca68e514..e0850df0f8 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php -@@ -108,5 +108,5 @@ class Container implements ContainerInterface, ResetInterface - * @throws InvalidArgumentException if the parameter is not defined +@@ -109,5 +109,5 @@ class Container implements ContainerInterface, ResetInterface + * @throws ParameterNotFoundException if the parameter is not defined */ - public function getParameter(string $name) + public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null { return $this->parameterBag->get($name); diff --git a/src/Symfony/Component/DependencyInjection/ContainerInterface.php b/src/Symfony/Component/DependencyInjection/ContainerInterface.php -index cad44026c0..14cd192e07 100644 +index 9e97fb71fc..1cda97c611 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerInterface.php +++ b/src/Symfony/Component/DependencyInjection/ContainerInterface.php @@ -53,5 +53,5 @@ interface ContainerInterface extends PsrContainerInterface - * @throws InvalidArgumentException if the parameter is not defined + * @throws ParameterNotFoundException if the parameter is not defined */ - public function getParameter(string $name); + public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null; @@ -358,7 +358,7 @@ index d553203c43..1163f4b107 100644 { $class = static::class; diff --git a/src/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php b/src/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php -index 4f66f18073..e96d867296 100644 +index 11cda00cc5..07b4990160 100644 --- a/src/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php +++ b/src/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php @@ -35,5 +35,5 @@ interface ExtensionInterface @@ -913,7 +913,7 @@ index 5014b9bd51..757c76f546 100644 + public function supportsDecoding(string $format): bool; } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php -index 391cdcb39c..f637687e74 100644 +index e426d87076..350cbc6335 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -213,5 +213,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn @@ -1094,11 +1094,11 @@ index b22d6ae609..31d1a25f9d 100644 + public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic): array { $refs = $values; -diff --git a/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php b/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php -index 471c1a6b91..2e19d2ab2d 100644 ---- a/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php -+++ b/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php -@@ -54,5 +54,5 @@ class GhostObjectState +diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +index 6ea89bc831..01748bcfc0 100644 +--- a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php ++++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +@@ -56,5 +56,5 @@ class LazyObjectState * @return bool Returns true when fully-initializing, false when partial-initializing */ - public function initialize($instance, $propertyName, $propertyScope) diff --git a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php index 52050c3a56d36..bcd16dc06e6f3 100644 --- a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php +++ b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php @@ -15,7 +15,7 @@ use ProxyManager\Proxy\GhostObjectInterface; use ProxyManager\Proxy\LazyLoadingInterface; use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\VarExporter\LazyGhostObjectInterface; +use Symfony\Component\VarExporter\LazyObjectInterface; /** * References Doctrine connections and entity/document managers. @@ -41,8 +41,8 @@ protected function resetService($name): void } $manager = $this->container->get($name); - if ($manager instanceof LazyGhostObjectInterface) { - if (!$manager->resetLazyGhostObject()) { + if ($manager instanceof LazyObjectInterface) { + if (!$manager->resetLazyObject()) { throw new \LogicException(sprintf('Resetting a non-lazy manager service is not supported. Declare the "%s" service as lazy.', $name)); } diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php index 85455d222194e..0967f1b1d5b23 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\TestCase; use ProxyManager\Proxy\LazyLoadingInterface; use ProxyManagerBridgeFooClass; +use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; use Symfony\Component\DependencyInjection\ContainerBuilder; /** @@ -29,6 +30,7 @@ class ContainerBuilderTest extends TestCase public function testCreateProxyServiceWithRuntimeInstantiator() { $builder = new ContainerBuilder(); + $builder->setProxyInstantiator(new RuntimeInstantiator()); $builder->register('foo1', ProxyManagerBridgeFooClass::class)->setFile(__DIR__.'/Fixtures/includes/foo.php')->setPublic(true); $builder->getDefinition('foo1')->setLazy(true)->addTag('proxy', ['interface' => ProxyManagerBridgeFooClass::class]); diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php index bef5e5062bb95..aedfff33c56c5 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use ProxyManager\Proxy\LazyLoadingInterface; +use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; @@ -66,6 +67,7 @@ private function dumpLazyServiceProjectServiceContainer() $container->compile(); $dumper = new PhpDumper($container); + $dumper->setProxyDumper(new ProxyDumper()); return $dumper->dump(['class' => 'LazyServiceProjectServiceContainer']); } diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index fe744789eaa50..274df06358bf1 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 6.2 --- - * Use lazy-loading ghost object proxies out of the box + * Use lazy-loading ghost objects and virtual proxies out of the box * Add argument `&$asGhostObject` to LazyProxy's `DumperInterface` to allow using ghost objects for lazy loading services * Add `enum` env var processor * Add `shuffle` env var processor diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 11376411c9091..cf0bd9ae244a0 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -24,9 +24,9 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; -use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; +use Symfony\Component\VarExporter\ProxyHelper; use Symfony\Contracts\Service\Attribute\SubscribedService; /** @@ -276,7 +276,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a continue; } - $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true); + $type = ProxyHelper::exportType($parameter, true); if ($checkAttributes) { foreach ($parameter->getAttributes() as $attribute) { @@ -306,8 +306,8 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a --$index; break; } - $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, false); - $type = $type ? sprintf('is type-hinted "%s"', ltrim($type, '\\')) : 'has no type-hint'; + $type = ProxyHelper::exportType($parameter); + $type = $type ? sprintf('is type-hinted "%s"', preg_replace('/(^|[(|&])\\\\|^\?\\\\?/', '\1', $type)) : 'has no type-hint'; throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" %s, you should configure its value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method, $type)); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php index 31924263504e5..0039496d72ec4 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php @@ -19,9 +19,9 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; -use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; +use Symfony\Component\VarExporter\ProxyHelper; /** * @author Guilhem Niot @@ -176,10 +176,11 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed continue; } - $typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter); + $typeHint = ltrim(ProxyHelper::exportType($parameter) ?? '', '?'); + $name = Target::parseName($parameter); - if ($typeHint && \array_key_exists($k = ltrim($typeHint, '\\').' $'.$name, $bindings)) { + if ($typeHint && \array_key_exists($k = preg_replace('/(^|[(|&])\\\\/', '\1', $typeHint).' $'.$name, $bindings)) { $arguments[$key] = $this->getBindingValue($bindings[$k]); continue; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php index 53bf4b2c8323b..28e4389de296c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php @@ -14,8 +14,8 @@ use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\VarExporter\ProxyHelper; /** * Resolves named arguments to their corresponding numeric index. @@ -87,7 +87,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $typeFound = false; foreach ($parameters as $j => $p) { - if (!\array_key_exists($j, $resolvedArguments) && ProxyHelper::getTypeHint($r, $p, true) === $key) { + if (!\array_key_exists($j, $resolvedArguments) && ProxyHelper::exportType($p, true) === $key) { $resolvedArguments[$j] = $argument; $typeFound = true; } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 7b6e40ea19b14..5f9bb9d1d8de7 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -46,7 +46,6 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; -use Symfony\Component\VarExporter\Hydrator; /** * ContainerBuilder is a DI container that provides an API to easily describe services. @@ -1037,10 +1036,6 @@ private function createService(Definition $definition, array &$inlineServices, b if (null !== $factory) { $service = $factory(...$arguments); - if (\is_object($tryProxy) && $service::class !== $parameterBag->resolveValue($definition->getClass())) { - throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1$s".', $definition->getClass(), get_debug_type($service))); - } - if (!$definition->isDeprecated() && \is_array($factory) && \is_string($factory[0])) { $r = new \ReflectionClass($factory[0]); @@ -1110,10 +1105,6 @@ private function createService(Definition $definition, array &$inlineServices, b $callable($service); } - if (\is_object($tryProxy) && $tryProxy !== $service) { - return Hydrator::hydrate($tryProxy, (array) $service); - } - return $service; } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 8fde0e73353bc..6f4465665812c 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -673,9 +673,6 @@ private function addServiceInstance(string $id, Definition $definition, bool $is $return = ''; if ($isSimpleInstance) { - if ($asGhostObject && null !== $definition->getFactory()) { - $instantiation .= '$this->hydrateProxy($lazyLoad, '; - } $return = 'return '; } else { $instantiation .= ' = '; @@ -893,9 +890,7 @@ protected function {$methodName}($lazyInitialization) $code .= sprintf(' %s ??= ', $factory); if ($asFile) { - $code .= "function () {\n"; - $code .= " return self::do(\$container);\n"; - $code .= " };\n\n"; + $code .= "fn () => self::do(\$container);\n\n"; } else { $code .= sprintf("\$this->%s(...);\n\n", $methodName); } @@ -1076,11 +1071,7 @@ private function addInlineService(string $id, Definition $definition, Definition return $code; } - if (!$asGhostObject) { - return $code."\n return \$instance;\n"; - } - - return $code."\n return \$this->hydrateProxy(\$lazyLoad, \$instance);\n"; + return $code."\n return \$instance;\n"; } private function addServices(array &$services = null): string @@ -1326,19 +1317,6 @@ protected function createProxy(\$class, \Closure \$factory) {$proxyLoader}return \$factory(); } - protected function hydrateProxy(\$proxy, \$instance) - { - if (\$proxy === \$instance) { - return \$proxy; - } - - if (!\in_array(\get_class(\$instance), [\get_class(\$proxy), get_parent_class(\$proxy)], true)) { - throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1\$s".', get_parent_class(\$proxy), get_debug_type(\$instance))); - } - - return \Symfony\Component\VarExporter\Hydrator::hydrate(\$proxy, (array) \$instance); - } - EOF; break; } diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php b/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php index 053f80f11bbec..419bcd5d54398 100644 --- a/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php @@ -11,12 +11,10 @@ namespace Symfony\Component\DependencyInjection\LazyProxy\Instantiator; -use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\LazyServiceDumper; -use Symfony\Component\VarExporter\LazyGhostObjectInterface; -use Symfony\Component\VarExporter\LazyGhostObjectTrait; +use Symfony\Component\VarExporter\LazyGhostTrait; /** * @author Nicolas Grekas @@ -27,14 +25,10 @@ public function instantiateProxy(ContainerInterface $container, Definition $defi { $dumper = new LazyServiceDumper(); - if ($dumper->useProxyManager($definition)) { - return (new RuntimeInstantiator())->instantiateProxy($container, $definition, $id, $realInstantiator); + if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $class), false)) { + eval($dumper->getProxyCode($definition)); } - if (!class_exists($proxyClass = $dumper->getProxyClass($definition), false)) { - eval(sprintf('class %s extends %s implements %s { use %s; }', $proxyClass, $definition->getClass(), LazyGhostObjectInterface::class, LazyGhostObjectTrait::class)); - } - - return $proxyClass::createLazyGhostObject($realInstantiator); + return isset(class_uses($proxyClass)[LazyGhostTrait::class]) ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator); } } diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php index ed66f6962db8b..0f443f42facd8 100644 --- a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php @@ -11,12 +11,10 @@ namespace Symfony\Component\DependencyInjection\LazyProxy\PhpDumper; -use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\DependencyInjection\Exception\LogicException; -use Symfony\Component\VarExporter\LazyGhostObjectInterface; -use Symfony\Component\VarExporter\LazyGhostObjectTrait; +use Symfony\Component\VarExporter\Exception\LogicException; +use Symfony\Component\VarExporter\ProxyHelper; /** * @author Nicolas Grekas @@ -48,29 +46,26 @@ public function isProxyCandidate(Definition $definition, bool &$asGhostObject = return false; } - $class = new \ReflectionClass($class); - - if ($class->isFinal()) { - throw new InvalidArgumentException(sprintf('Cannot make service of class "%s" lazy because the class is final.', $definition->getClass())); + if ($definition->getFactory()) { + return true; } - if ($asGhostObject = !$class->isAbstract() && !$class->isInterface() && (\stdClass::class === $class->name || !$class->isInternal())) { - while ($class = $class->getParentClass()) { - if (!$asGhostObject = \stdClass::class === $class->name || !$class->isInternal()) { - break; - } + foreach ($definition->getMethodCalls() as $call) { + if ($call[2] ?? false) { + return true; } } + try { + $asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class)); + } catch (LogicException) { + } + return true; } public function getProxyFactoryCode(Definition $definition, string $id, string $factoryCode): string { - if ($dumper = $this->useProxyManager($definition)) { - return $dumper->getProxyFactoryCode($definition, $id, $factoryCode); - } - $instantiation = 'return'; if ($definition->isShared()) { @@ -79,66 +74,75 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $ $proxyClass = $this->getProxyClass($definition); + if (!str_contains($factoryCode, '$proxy')) { + return <<createProxy('$proxyClass', fn () => \\$proxyClass::createLazyProxy(fn () => $factoryCode)); + } + + + EOF; + } + if (preg_match('/^\$this->\w++\(\$proxy\)$/', $factoryCode)) { $factoryCode = substr_replace($factoryCode, '(...)', -8); } else { - $factoryCode = sprintf('function ($proxy) { return %s; }', $factoryCode); + $factoryCode = sprintf('fn ($proxy) => %s', $factoryCode); } return <<createProxy('$proxyClass', function () { - return \\$proxyClass::createLazyGhostObject($factoryCode); - }); - } + if (true === \$lazyLoad) { + $instantiation \$this->createProxy('$proxyClass', fn () => \\$proxyClass::createLazyGhost($factoryCode)); + } -EOF; + EOF; } public function getProxyCode(Definition $definition): string - { - if ($dumper = $this->useProxyManager($definition)) { - return $dumper->getProxyCode($definition); - } - - $proxyClass = $this->getProxyClass($definition); - - return sprintf(<<getClass(), - LazyGhostObjectInterface::class, - LazyGhostObjectTrait::class - ); - } - - public function getProxyClass(Definition $definition): string - { - $class = (new \ReflectionClass($definition->getClass()))->name; - - return preg_replace('/^.*\\\\/', '', $class).'_'.substr(hash('sha256', $this->salt.'+'.$class), -7); - } - - public function useProxyManager(Definition $definition): ?ProxyDumper { if (!$this->isProxyCandidate($definition, $asGhostObject)) { throw new InvalidArgumentException(sprintf('Cannot instantiate lazy proxy for service of class "%s".', $definition->getClass())); } + $proxyClass = $this->getProxyClass($definition, $class); if ($asGhostObject) { - return null; + try { + return 'class '.$proxyClass.ProxyHelper::generateLazyGhost($class); + } catch (LogicException $e) { + throw new InvalidArgumentException(sprintf('Cannot generate lazy ghost for service of class "%s" lazy.', $definition->getClass()), 0, $e); + } } - if (!class_exists(ProxyDumper::class)) { - throw new LogicException('You cannot use virtual proxies for lazy services as the ProxyManager bridge is not installed. Try running "composer require symfony/proxy-manager-bridge".'); + if ($definition->hasTag('proxy')) { + $interfaces = []; + foreach ($definition->getTag('proxy') as $tag) { + if (!isset($tag['interface'])) { + throw new InvalidArgumentException(sprintf('Invalid definition for service of class "%s": the "interface" attribute is missing on a "proxy" tag.', $definition->getClass())); + } + if (!interface_exists($tag['interface']) && !class_exists($tag['interface'], false)) { + throw new InvalidArgumentException(sprintf('Invalid definition for service of class "%s": several "proxy" tags found but "%s" is not an interface.', $definition->getClass(), $tag['interface'])); + } + $interfaces[] = new \ReflectionClass($tag['interface']); + } + } else { + $interfaces = [$class]; } + if (1 === \count($interfaces) && !$interfaces[0]->isInterface()) { + $class = array_pop($interfaces); + } + + try { + return (\PHP_VERSION_ID >= 80200 && $class->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyProxy($class, $interfaces); + } catch (LogicException $e) { + throw new InvalidArgumentException(sprintf('Cannot generate lazy proxy for service of class "%s" lazy.', $definition->getClass()), 0, $e); + } + } + + public function getProxyClass(Definition $definition, \ReflectionClass &$class = null): string + { + $class = new \ReflectionClass($definition->getClass()); - return new ProxyDumper($this->salt); + return preg_replace('/^.*\\\\/', '', $class->name).'_'.substr(hash('sha256', $this->salt.'+'.$class->name), -7); } } diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php b/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php index f33011ad1d84f..bde7d6a3fff58 100644 --- a/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php @@ -11,10 +11,12 @@ namespace Symfony\Component\DependencyInjection\LazyProxy; +trigger_deprecation('symfony/dependency-injection', '6.2', 'The "%s" class is deprecated, use "%s" instead.', ProxyHelper::class, \Symfony\Component\VarExporter\ProxyHelper::class); + /** * @author Nicolas Grekas * - * @internal + * @deprecated since Symfony 6.2, use VarExporter's ProxyHelper instead */ class ProxyHelper { diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 5f48b68e2c046..8325ecb51b7e5 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1696,7 +1696,8 @@ public function testLazyWither() $wither = $container->get('wither'); $this->assertInstanceOf(Foo::class, $wither->foo); - $this->assertTrue($wither->resetLazyGhostObject()); + $this->assertTrue($wither->resetLazyObject()); + $this->assertInstanceOf(Wither::class, $wither->withFoo1($wither->foo)); } public function testWitherWithStaticReturnType() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 47efc42331fd1..316c89d461e37 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -1452,7 +1452,7 @@ public function testLazyWither() $wither = $container->get('wither'); $this->assertInstanceOf(Foo::class, $wither->foo); - $this->assertTrue($wither->resetLazyGhostObject()); + $this->assertTrue($wither->resetLazyObject()); } public function testWitherWithStaticReturnType() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt index 4c13b02341884..7af59984549ae 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt @@ -6,7 +6,7 @@ namespace Container%s; include_once $this->targetDir.''.'/Fixtures/includes/foo.php'; -class FooClass_2b16075 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyGhostObjectInterface +class FooClass_2b16075 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface %A if (!\class_exists('FooClass_%s', false)) { @@ -70,19 +70,6 @@ class ProjectServiceContainer extends Container return $factory(); } - protected function hydrateProxy($proxy, $instance) - { - if ($proxy === $instance) { - return $proxy; - } - - if (!\in_array(\get_class($instance), [\get_class($proxy), get_parent_class($proxy)], true)) { - throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1$s".', get_parent_class($proxy), get_debug_type($instance))); - } - - return \Symfony\Component\VarExporter\Hydrator::hydrate($proxy, (array) $instance); - } - /** * Gets the public 'lazy_foo' shared service. * @@ -91,9 +78,7 @@ class ProjectServiceContainer extends Container protected function getLazyFooService($lazyLoad = true) { if (true === $lazyLoad) { - return $this->services['lazy_foo'] = $this->createProxy('FooClass_2b16075', function () { - return \FooClass_2b16075::createLazyGhostObject($this->getLazyFooService(...)); - }); + return $this->services['lazy_foo'] = $this->createProxy('FooClass_2b16075', fn () => \FooClass_2b16075::createLazyGhost($this->getLazyFooService(...))); } include_once $this->targetDir.''.'/Fixtures/includes/foo_lazy.php'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_ghost.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_ghost.php index a918805905491..32e5364ef4e35 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_ghost.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_ghost.php @@ -42,19 +42,6 @@ protected function createProxy($class, \Closure $factory) return $factory(); } - protected function hydrateProxy($proxy, $instance) - { - if ($proxy === $instance) { - return $proxy; - } - - if (!\in_array(\get_class($instance), [\get_class($proxy), get_parent_class($proxy)], true)) { - throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1$s".', get_parent_class($proxy), get_debug_type($instance))); - } - - return \Symfony\Component\VarExporter\Hydrator::hydrate($proxy, (array) $instance); - } - /** * Gets the public 'bar' shared service. * @@ -63,9 +50,7 @@ protected function hydrateProxy($proxy, $instance) protected function getBarService($lazyLoad = true) { if (true === $lazyLoad) { - return $this->services['bar'] = $this->createProxy('stdClass_5a8a5eb', function () { - return \stdClass_5a8a5eb::createLazyGhostObject($this->getBarService(...)); - }); + return $this->services['bar'] = $this->createProxy('stdClass_5a8a5eb', fn () => \stdClass_5a8a5eb::createLazyGhost($this->getBarService(...))); } return $lazyLoad; @@ -79,16 +64,23 @@ protected function getBarService($lazyLoad = true) protected function getFooService($lazyLoad = true) { if (true === $lazyLoad) { - return $this->services['foo'] = $this->createProxy('stdClass_5a8a5eb', function () { - return \stdClass_5a8a5eb::createLazyGhostObject($this->getFooService(...)); - }); + return $this->services['foo'] = $this->createProxy('stdClass_5a8a5eb', fn () => \stdClass_5a8a5eb::createLazyGhost($this->getFooService(...))); } return $lazyLoad; } } -class stdClass_5a8a5eb extends \stdClass implements \Symfony\Component\VarExporter\LazyGhostObjectInterface +class stdClass_5a8a5eb extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface { - use \Symfony\Component\VarExporter\LazyGhostObjectTrait; + use \Symfony\Component\VarExporter\LazyGhostTrait; + + private int $lazyObjectId; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; } + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_proxy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_proxy.php index c1e8c37b5fc4c..841c892216f68 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_proxy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_proxy.php @@ -42,19 +42,6 @@ protected function createProxy($class, \Closure $factory) return $factory(); } - protected function hydrateProxy($proxy, $instance) - { - if ($proxy === $instance) { - return $proxy; - } - - if (!\in_array(\get_class($instance), [\get_class($proxy), get_parent_class($proxy)], true)) { - throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1$s".', get_parent_class($proxy), get_debug_type($instance))); - } - - return \Symfony\Component\VarExporter\Hydrator::hydrate($proxy, (array) $instance); - } - /** * Gets the public 'bar' shared service. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy.php index ea4a634841684..001d7746da3bb 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy.php @@ -48,19 +48,6 @@ protected function createProxy($class, \Closure $factory) return $factory(); } - protected function hydrateProxy($proxy, $instance) - { - if ($proxy === $instance) { - return $proxy; - } - - if (!\in_array(\get_class($instance), [\get_class($proxy), get_parent_class($proxy)], true)) { - throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1$s".', get_parent_class($proxy), get_debug_type($instance))); - } - - return \Symfony\Component\VarExporter\Hydrator::hydrate($proxy, (array) $instance); - } - /** * Gets the public 'bar' shared service. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt index 889d92125785e..d04e886a810b2 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt @@ -19,14 +19,10 @@ class getNonSharedFooService extends ProjectServiceContainer */ public static function do($container, $lazyLoad = true) { - $container->factories['non_shared_foo'] ??= function () use ($container) { - return self::do($container); - }; + $container->factories['non_shared_foo'] ??= fn () => self::do($container); if (true === $lazyLoad) { - return $container->createProxy('FooLazyClass_f814e3a', function () use ($container) { - return \FooLazyClass_f814e3a::createLazyGhostObject(function ($proxy) use ($container) { return self::do($container, $proxy); }); - }); + return $container->createProxy('FooLazyClass_f814e3a', fn () => \FooLazyClass_f814e3a::createLazyGhost(fn ($proxy) => self::do($container, $proxy))); } static $include = true; @@ -45,11 +41,20 @@ class getNonSharedFooService extends ProjectServiceContainer namespace Container%s; -class FooLazyClass_f814e3a extends \Bar\FooLazyClass implements \Symfony\Component\VarExporter\LazyGhostObjectInterface +class FooLazyClass_f814e3a extends \Bar\FooLazyClass implements \Symfony\Component\VarExporter\LazyObjectInterface { - use \Symfony\Component\VarExporter\LazyGhostObjectTrait; + use \Symfony\Component\VarExporter\LazyGhostTrait; + + private int $lazyObjectId; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; } +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); + if (!\class_exists('FooLazyClass_f814e3a', false)) { \class_alias(__NAMESPACE__.'\\FooLazyClass_f814e3a', 'FooLazyClass_f814e3a', false); } @@ -123,19 +128,6 @@ class ProjectServiceContainer extends Container return $factory(); } - - protected function hydrateProxy($proxy, $instance) - { - if ($proxy === $instance) { - return $proxy; - } - - if (!\in_array(\get_class($instance), [\get_class($proxy), get_parent_class($proxy)], true)) { - throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1$s".', get_parent_class($proxy), get_debug_type($instance))); - } - - return \Symfony\Component\VarExporter\Hydrator::hydrate($proxy, (array) $instance); - } } [ProjectServiceContainer.preload.php] => factories['service_container']['foo'] ??= $this->getFooService(...); if (true === $lazyLoad) { - return $this->createProxy('stdClass_5a8a5eb', function () { - return \stdClass_5a8a5eb::createLazyGhostObject($this->getFooService(...)); - }); + return $this->createProxy('stdClass_5a8a5eb', fn () => \stdClass_5a8a5eb::createLazyGhost($this->getFooService(...))); } return $lazyLoad; } } -class stdClass_5a8a5eb extends \stdClass implements \Symfony\Component\VarExporter\LazyGhostObjectInterface +class stdClass_5a8a5eb extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface { - use \Symfony\Component\VarExporter\LazyGhostObjectTrait; + use \Symfony\Component\VarExporter\LazyGhostTrait; + + private int $lazyObjectId; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; } + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php index e21fc1d7fdc87..ec39e663e4ec7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php @@ -48,19 +48,6 @@ protected function createProxy($class, \Closure $factory) return $factory(); } - protected function hydrateProxy($proxy, $instance) - { - if ($proxy === $instance) { - return $proxy; - } - - if (!\in_array(\get_class($instance), [\get_class($proxy), get_parent_class($proxy)], true)) { - throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1$s".', get_parent_class($proxy), get_debug_type($instance))); - } - - return \Symfony\Component\VarExporter\Hydrator::hydrate($proxy, (array) $instance); - } - /** * Gets the public 'wither' shared autowired service. * @@ -69,12 +56,10 @@ protected function hydrateProxy($proxy, $instance) protected function getWitherService($lazyLoad = true) { if (true === $lazyLoad) { - return $this->services['wither'] = $this->createProxy('Wither_94fa281', function () { - return \Wither_94fa281::createLazyGhostObject($this->getWitherService(...)); - }); + return $this->services['wither'] = $this->createProxy('Wither_94fa281', fn () => \Wither_94fa281::createLazyProxy(fn () => $this->getWitherService(false))); } - $instance = $lazyLoad; + $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); $a = new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo(); @@ -82,11 +67,25 @@ protected function getWitherService($lazyLoad = true) $instance = $instance->withFoo2($a); $instance->setFoo($a); - return $this->hydrateProxy($lazyLoad, $instance); + return $instance; } } -class Wither_94fa281 extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyGhostObjectInterface +class Wither_94fa281 extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface { - use \Symfony\Component\VarExporter\LazyGhostObjectTrait; + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private int $lazyObjectId; + private parent $lazyObjectReal; + + private const LAZY_OBJECT_PROPERTY_SCOPES = [ + 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], + "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null], + 'foo' => [parent::class, 'foo', null], + ]; } + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php index de0f1ca052603..5befc7fc8cbe9 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -21,7 +21,7 @@ use Prophecy\Prophecy\ProphecySubjectInterface; use ProxyManager\Proxy\ProxyInterface; use Symfony\Component\ErrorHandler\Internal\TentativeTypes; -use Symfony\Component\VarExporter\LazyGhostObjectInterface; +use Symfony\Component\VarExporter\LazyObjectInterface; /** * Autoloader checking if the class is really defined in the file found. @@ -251,7 +251,7 @@ public static function checkClasses(): bool && !is_subclass_of($symbols[$i], ProphecySubjectInterface::class) && !is_subclass_of($symbols[$i], Proxy::class) && !is_subclass_of($symbols[$i], ProxyInterface::class) - && !is_subclass_of($symbols[$i], LazyGhostObjectInterface::class) + && !is_subclass_of($symbols[$i], LazyObjectInterface::class) && !is_subclass_of($symbols[$i], LegacyProxy::class) && !is_subclass_of($symbols[$i], MockInterface::class) && !is_subclass_of($symbols[$i], IMock::class) diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index c266917e91290..dd58485996f0b 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -19,11 +19,11 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\VarExporter\ProxyHelper; /** * Creates the service-locators required by ServiceValueResolver. @@ -120,7 +120,7 @@ public function process(ContainerBuilder $container) $args = []; foreach ($parameters as $p) { /** @var \ReflectionParameter $p */ - $type = ltrim($target = (string) ProxyHelper::getTypeHint($r, $p), '\\'); + $type = preg_replace('/(^|[(|&])\\\\/', '\1', $target = ltrim(ProxyHelper::exportType($p) ?? '', '?')); $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; $autowireAttributes = $autowire ? $emptyAutowireAttributes : []; @@ -183,7 +183,7 @@ public function process(ContainerBuilder $container) $args[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE); } else { - $target = ltrim($target, '\\'); + $target = preg_replace('/(^|[(|&])\\\\/', '\1', $target); $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, Target::parseName($p)) : new Reference($target, $invalidBehavior); } } diff --git a/src/Symfony/Component/VarExporter/CHANGELOG.md b/src/Symfony/Component/VarExporter/CHANGELOG.md index 25f2a55b5ff78..1b21a0bbde8cc 100644 --- a/src/Symfony/Component/VarExporter/CHANGELOG.md +++ b/src/Symfony/Component/VarExporter/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 6.2 --- - * Add `LazyGhostObjectTrait` + * Add support for lazy ghost objects and virtual proxies * Add `Hydrator::hydrate()` * Preserve PHP references also when using `Hydrator::hydrate()` or `Instantiator::instantiate()` * Add support for hydrating from native (array) casts diff --git a/src/Symfony/Component/VarExporter/Internal/EmptyScope.php b/src/Symfony/Component/VarExporter/Exception/LogicException.php similarity index 65% rename from src/Symfony/Component/VarExporter/Internal/EmptyScope.php rename to src/Symfony/Component/VarExporter/Exception/LogicException.php index 224f6b96452fc..619d0559ab819 100644 --- a/src/Symfony/Component/VarExporter/Internal/EmptyScope.php +++ b/src/Symfony/Component/VarExporter/Exception/LogicException.php @@ -9,11 +9,8 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Internal; +namespace Symfony\Component\VarExporter\Exception; -/** - * @internal - */ -enum EmptyScope +class LogicException extends \LogicException implements ExceptionInterface { } diff --git a/src/Symfony/Component/VarExporter/Internal/GhostObjectId.php b/src/Symfony/Component/VarExporter/Internal/GhostObjectId.php deleted file mode 100644 index 3cc614d21322d..0000000000000 --- a/src/Symfony/Component/VarExporter/Internal/GhostObjectId.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarExporter\Internal; - -/** - * Keeps the state of lazy ghost objects. - * - * As a micro-optimization, this class uses no type declarations. - * - * @internal - */ -class GhostObjectId -{ - public int $id; - - public function __construct() - { - $this->id = spl_object_id($this); - } - - public function __clone() - { - $this->id = spl_object_id($this); - } - - public function __destruct() - { - unset(GhostObjectRegistry::$states[$this->id]); - } -} diff --git a/src/Symfony/Component/VarExporter/Internal/GhostObjectRegistry.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php similarity index 60% rename from src/Symfony/Component/VarExporter/Internal/GhostObjectRegistry.php rename to src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php index 757ce1b7b604e..888f90025bc84 100644 --- a/src/Symfony/Component/VarExporter/Internal/GhostObjectRegistry.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php @@ -12,16 +12,16 @@ namespace Symfony\Component\VarExporter\Internal; /** - * Stores the state of lazy ghost objects and caches related reflection information. + * Stores the state of lazy objects and caches related reflection information. * * As a micro-optimization, this class uses no type declarations. * * @internal */ -class GhostObjectRegistry +class LazyObjectRegistry { /** - * @var array + * @var array */ public static $states = []; @@ -46,17 +46,24 @@ class GhostObjectRegistry public static $classAccessors = []; /** - * @var array + * @var array */ public static $parentMethods = []; public static function getClassResetters($class) { $classProperties = []; - $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); + + if ((self::$classReflectors[$class] ??= new \ReflectionClass($class))->isInternal()) { + $propertyScopes = []; + } else { + $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); + } foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { - if ('lazyGhostObjectId' !== $name && null !== ($propertyScopes["\0$scope\0$name"] ?? $propertyScopes["\0*\0$name"] ?? $readonlyScope)) { + $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = null !== $readonlyScope ? $name : null; + + if ($k === $key && "\0$class\0lazyObjectId" !== $k) { $classProperties[$readonlyScope ?? $scope][$name] = $key; } } @@ -105,22 +112,40 @@ public static function getClassAccessors($class) unset($instance->$name); }, ]; - }, null, $class)(); + }, null, \Closure::class === $class ? null : $class)(); } public static function getParentMethods($class) { $parent = get_parent_class($class); + $methods = []; + + foreach (['set', 'isset', 'unset', 'clone', 'serialize', 'unserialize', 'sleep', 'wakeup', 'destruct', 'get'] as $method) { + if (!$parent || !method_exists($parent, '__'.$method)) { + $methods[$method] = false; + } else { + $m = new \ReflectionMethod($parent, '__'.$method); + $methods[$method] = !$m->isAbstract() && !$m->isPrivate(); + } + } + + $methods['get'] = $methods['get'] ? ($m->returnsReference() ? 2 : 1) : 0; + + return $methods; + } + + public static function getScope($propertyScopes, $class, $property, $readonlyScope = null) + { + if (null === $readonlyScope && !isset($propertyScopes["\0$class\0$property"]) && !isset($propertyScopes["\0*\0$property"])) { + return null; + } + + $scope = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['class'] ?? \Closure::class; + + if (null === $readonlyScope && isset($propertyScopes["\0*\0$property"]) && ($class === $scope || is_subclass_of($class, $scope))) { + return null; + } - return [ - 'get' => $parent && method_exists($parent, '__get') ? ((new \ReflectionMethod($parent, '__get'))->returnsReference() ? 2 : 1) : 0, - 'set' => $parent && method_exists($parent, '__set'), - 'isset' => $parent && method_exists($parent, '__isset'), - 'unset' => $parent && method_exists($parent, '__unset'), - 'clone' => $parent && method_exists($parent, '__clone'), - 'serialize' => $parent && method_exists($parent, '__serialize'), - 'sleep' => $parent && method_exists($parent, '__sleep'), - 'destruct' => $parent && method_exists($parent, '__destruct'), - ]; + return $scope; } } diff --git a/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php similarity index 59% rename from src/Symfony/Component/VarExporter/Internal/GhostObjectState.php rename to src/Symfony/Component/VarExporter/Internal/LazyObjectState.php index 509cac3d50275..a6cd95741e3f3 100644 --- a/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php @@ -14,41 +14,47 @@ use Symfony\Component\VarExporter\Hydrator; /** - * Keeps the state of lazy ghost objects. + * Keeps the state of lazy objects. * * As a micro-optimization, this class uses no type declarations. * * @internal */ -class GhostObjectState +class LazyObjectState { public const STATUS_INITIALIZED_PARTIAL = 1; public const STATUS_UNINITIALIZED_FULL = 2; public const STATUS_INITIALIZED_FULL = 3; - public \Closure $initializer; - /** * @var array> */ - public $preInitUnsetProperties; + public array $preInitUnsetProperties; /** * @var array */ - public $preInitSetProperties = []; + public array $preInitSetProperties; /** * @var array> */ - public $unsetProperties = []; + public array $unsetProperties; + + /** + * @var array + */ + public array $skippedProperties; /** - * One of self::STATUS_*. - * - * @var int + * @var self::STATUS_* */ - public $status; + public int $status = 0; + + public function __construct(public \Closure $initializer, $skippedProperties = []) + { + $this->skippedProperties = $this->preInitSetProperties = $skippedProperties; + } /** * @return bool Returns true when fully-initializing, false when partial-initializing @@ -57,7 +63,15 @@ public function initialize($instance, $propertyName, $propertyScope) { if (!$this->status) { $this->status = 1 < (new \ReflectionFunction($this->initializer))->getNumberOfRequiredParameters() ? self::STATUS_INITIALIZED_PARTIAL : self::STATUS_UNINITIALIZED_FULL; - $this->preInitUnsetProperties ??= $this->unsetProperties; + $this->preInitUnsetProperties = $this->unsetProperties ??= []; + + if (\count($this->preInitSetProperties) !== \count($properties = $this->preInitSetProperties + (array) $instance)) { + $this->preInitSetProperties = array_fill_keys(array_keys($properties), true); + } + + if (null === $propertyName) { + return self::STATUS_INITIALIZED_PARTIAL !== $this->status; + } } if (self::STATUS_INITIALIZED_FULL === $this->status) { @@ -65,7 +79,7 @@ public function initialize($instance, $propertyName, $propertyScope) } if (self::STATUS_UNINITIALIZED_FULL === $this->status) { - if ($defaultProperties = array_diff_key(GhostObjectRegistry::$defaultProperties[$instance::class], (array) $instance)) { + if ($defaultProperties = array_diff_key(LazyObjectRegistry::$defaultProperties[$instance::class], $this->preInitSetProperties)) { Hydrator::hydrate($instance, $defaultProperties); } @@ -78,7 +92,7 @@ public function initialize($instance, $propertyName, $propertyScope) $value = ($this->initializer)(...[$instance, $propertyName, $propertyScope]); $propertyScope ??= $instance::class; - $accessor = GhostObjectRegistry::$classAccessors[$propertyScope] ??= GhostObjectRegistry::getClassAccessors($propertyScope); + $accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope); $accessor['set']($instance, $propertyName, $value); diff --git a/src/Symfony/Component/VarExporter/LazyGhostObjectTrait.php b/src/Symfony/Component/VarExporter/LazyGhostTrait.php similarity index 63% rename from src/Symfony/Component/VarExporter/LazyGhostObjectTrait.php rename to src/Symfony/Component/VarExporter/LazyGhostTrait.php index 658bae08191af..f270e9c0b31ad 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostObjectTrait.php +++ b/src/Symfony/Component/VarExporter/LazyGhostTrait.php @@ -11,45 +11,49 @@ namespace Symfony\Component\VarExporter; -use Symfony\Component\VarExporter\Internal\EmptyScope; -use Symfony\Component\VarExporter\Internal\GhostObjectId; -use Symfony\Component\VarExporter\Internal\GhostObjectRegistry as Registry; -use Symfony\Component\VarExporter\Internal\GhostObjectState; use Symfony\Component\VarExporter\Internal\Hydrator; +use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry; +use Symfony\Component\VarExporter\Internal\LazyObjectState; -trait LazyGhostObjectTrait +/** + * @property int $lazyObjectId + */ +trait LazyGhostTrait { - private ?GhostObjectId $lazyGhostObjectId = null; - /** * @param \Closure(static):void|\Closure(static, string, ?string):mixed $initializer Initializes the instance passed as argument; when partial initialization * is desired the closure should take extra arguments $propertyName and * $propertyScope and should return the value of the corresponding property + * @param array $skippedProperties An array indexed by the properties to skip, + * aka the ones that the initializer doesn't set */ - public static function createLazyGhostObject(\Closure $initializer): static + public static function createLazyGhost(\Closure $initializer, array $skippedProperties = []): static { - $class = static::class; - $instance = (Registry::$classReflectors[$class] ??= new \ReflectionClass($class))->newInstanceWithoutConstructor(); + if (self::class !== $class = static::class) { + $skippedProperties["\0".self::class."\0lazyObjectId"] = true; + } elseif (\defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) { + Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES; + } + $instance = (Registry::$classReflectors[$class] ??= new \ReflectionClass($class))->newInstanceWithoutConstructor(); Registry::$defaultProperties[$class] ??= (array) $instance; - $instance->lazyGhostObjectId = new GhostObjectId(); - $state = Registry::$states[$instance->lazyGhostObjectId->id] = new GhostObjectState(); - $state->initializer = $initializer; + $instance->lazyObjectId = $id = spl_object_id($instance); + Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties); foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) { - $reset($instance); + $reset($instance, $skippedProperties); } return $instance; } /** - * Forces initialization of a lazy ghost object. + * Forces initialization of a lazy object and returns it. */ - public function initializeLazyGhostObject(): void + public function initializeLazyObject(): static { - if (!$state = Registry::$states[$this->lazyGhostObjectId?->id] ?? null) { - return; + if (!$state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) { + return $this; } $class = static::class; @@ -62,24 +66,25 @@ public function initializeLazyGhostObject(): void continue; } if ($state->initialize($this, $name, $readonlyScope ?? ('*' !== $scope ? $scope : null))) { - return; + return $this; } $properties = (array) $this; } + + return $this; } /** - * @return bool Returns false when the object cannot be reset, ie when it's not a ghost object + * @return bool Returns false when the object cannot be reset, ie when it's not a lazy object */ - public function resetLazyGhostObject(): bool + public function resetLazyObject(): bool { - if (!$state = Registry::$states[$this->lazyGhostObjectId?->id] ?? null) { + if (!$state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) { return false; } if (!$state->status) { - $state->preInitSetProperties = []; - $state->preInitUnsetProperties ??= $state->unsetProperties ?? []; + return $state->initialize($this, null, null) || true; } $class = static::class; @@ -97,30 +102,24 @@ public function resetLazyGhostObject(): bool $reset($this, $skippedProperties); } - if (GhostObjectState::STATUS_INITIALIZED_FULL === $state->status) { - $state->status = GhostObjectState::STATUS_UNINITIALIZED_FULL; + if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) { + $state->status = LazyObjectState::STATUS_UNINITIALIZED_FULL; } - $state->unsetProperties = $state->preInitUnsetProperties; + $state->unsetProperties = $state->preInitUnsetProperties ??= []; return true; } - public function &__get($name) + public function &__get($name): mixed { $propertyScopes = Hydrator::$propertyScopes[static::class] ??= Hydrator::getPropertyScopes(static::class); $scope = null; if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - if (isset($propertyScopes["\0$class\0$name"]) || isset($propertyScopes["\0*\0$name"])) { - $scope = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? EmptyScope::class; + $scope = Registry::getScope($propertyScopes, $class, $name); - if (isset($propertyScopes["\0*\0$name"]) && ($class === $scope || is_subclass_of($class, $scope))) { - $scope = null; - } - } - - if ($state = Registry::$states[$this->lazyGhostObjectId?->id] ?? null) { + if ($state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) { if (isset($state->unsetProperties[$scope ?? '*'][$name])) { $class = null; } elseif (null === $scope || isset($propertyScopes["\0$scope\0$name"])) { @@ -132,10 +131,9 @@ public function &__get($name) if ($parent = (Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['get']) { if (2 === $parent) { - $value = &parent::__get($name); - } else { - $value = parent::__get($name); + return parent::__get($name); } + $value = parent::__get($name); return $value; } @@ -167,21 +165,10 @@ public function __set($name, $value): void $state = null; if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - if (null !== $readonlyScope || isset($propertyScopes["\0$class\0$name"]) || isset($propertyScopes["\0*\0$name"])) { - $scope = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? EmptyScope::class; + $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); - if (null === $readonlyScope && isset($propertyScopes["\0*\0$name"]) && ($class === $scope || is_subclass_of($class, $scope))) { - $scope = null; - } - } - - $state = Registry::$states[$this->lazyGhostObjectId?->id] ?? null; + $state = Registry::$states[$this->lazyObjectId ?? ''] ?? null; if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))) { - if (!$state->status && null === $state->preInitUnsetProperties) { - $propertyScopes[$k = "\0$class\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; - $state->preInitSetProperties[$k] = true; - } - goto set_in_scope; } } @@ -212,15 +199,9 @@ public function __isset($name): bool $scope = null; if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - if (isset($propertyScopes["\0$class\0$name"]) || isset($propertyScopes["\0*\0$name"])) { - $scope = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? EmptyScope::class; - - if (isset($propertyScopes["\0*\0$name"]) && ($class === $scope || is_subclass_of($class, $scope))) { - $scope = null; - } - } + $scope = Registry::getScope($propertyScopes, $class, $name); - if ($state = Registry::$states[$this->lazyGhostObjectId?->id] ?? null) { + if ($state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) { if (isset($state->unsetProperties[$scope ?? '*'][$name])) { return false; } @@ -252,20 +233,10 @@ public function __unset($name): void $scope = null; if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - if (null !== $readonlyScope || isset($propertyScopes["\0$class\0$name"]) || isset($propertyScopes["\0*\0$name"])) { - $scope = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? EmptyScope::class; - - if (null === $readonlyScope && isset($propertyScopes["\0*\0$name"]) && ($class === $scope || is_subclass_of($class, $scope))) { - $scope = null; - } - } + $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); - $state = Registry::$states[$this->lazyGhostObjectId?->id] ?? null; + $state = Registry::$states[$this->lazyObjectId ?? ''] ?? null; if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))) { - if (!$state->status && null === $state->preInitUnsetProperties) { - $propertyScopes[$k = "\0$class\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; - unset($state->preInitSetProperties[$k]); - } $state->unsetProperties[$scope ?? '*'][$name] = true; return; @@ -288,11 +259,14 @@ public function __unset($name): void $accessor['unset']($this, $name); } - public function __clone() + public function __clone(): void { - if ($previousId = $this->lazyGhostObjectId?->id) { - $this->lazyGhostObjectId = clone $this->lazyGhostObjectId; - Registry::$states[$this->lazyGhostObjectId->id] = clone Registry::$states[$previousId]; + if ($state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) { + Registry::$states[$this->lazyObjectId = spl_object_id($this)] = clone $state; + + if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) { + return; + } } if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['clone']) { @@ -305,14 +279,14 @@ public function __serialize(): array $class = self::class; if ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['serialize']) { - return parent::__serialize(); + $properties = parent::__serialize(); + } else { + $this->initializeLazyObject(); + $properties = (array) $this; } + unset($properties["\0$class\0lazyObjectId"]); - $this->initializeLazyGhostObject(); - $properties = (array) $this; - unset($properties["\0$class\0lazyGhostObjectId"]); - - if (!Registry::$parentMethods[$class]['sleep']) { + if (Registry::$parentMethods[$class]['serialize'] || !Registry::$parentMethods[$class]['sleep']) { return $properties; } @@ -334,12 +308,20 @@ public function __serialize(): array public function __destruct() { - if (!(Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['destruct']) { - return; - } + $state = Registry::$states[$this->lazyObjectId ?? null] ?? null; - if ((Registry::$states[$this->lazyGhostObjectId?->id] ?? null)?->status) { - parent::__destruct(); + try { + if ($state && !\in_array($state->status, [LazyObjectState::STATUS_INITIALIZED_FULL, LazyObjectState::STATUS_INITIALIZED_PARTIAL], true)) { + return; + } + + if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['destruct']) { + parent::__destruct(); + } + } finally { + if ($state) { + unset(Registry::$states[$this->lazyObjectId]); + } } } } diff --git a/src/Symfony/Component/VarExporter/LazyGhostObjectInterface.php b/src/Symfony/Component/VarExporter/LazyObjectInterface.php similarity index 62% rename from src/Symfony/Component/VarExporter/LazyGhostObjectInterface.php rename to src/Symfony/Component/VarExporter/LazyObjectInterface.php index 361d2766b8b42..47d373f3b4baf 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostObjectInterface.php +++ b/src/Symfony/Component/VarExporter/LazyObjectInterface.php @@ -11,15 +11,15 @@ namespace Symfony\Component\VarExporter; -interface LazyGhostObjectInterface +interface LazyObjectInterface { /** - * Forces initialization of a lazy ghost object. + * Forces initialization of a lazy object and returns it. */ - public function initializeLazyGhostObject(): void; + public function initializeLazyObject(): object; /** - * @return bool Returns false when the object cannot be reset, ie when it's not a ghost object + * @return bool Returns false when the object cannot be reset, ie when it's not a lazy object */ - public function resetLazyGhostObject(): bool; + public function resetLazyObject(): bool; } diff --git a/src/Symfony/Component/VarExporter/LazyProxyTrait.php b/src/Symfony/Component/VarExporter/LazyProxyTrait.php new file mode 100644 index 0000000000000..568d92f0a6fa3 --- /dev/null +++ b/src/Symfony/Component/VarExporter/LazyProxyTrait.php @@ -0,0 +1,345 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter; + +use Symfony\Component\VarExporter\Hydrator as PublicHydrator; +use Symfony\Component\VarExporter\Internal\Hydrator; +use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry; +use Symfony\Component\VarExporter\Internal\LazyObjectState; + +/** + * @property int $lazyObjectId + * @property parent $lazyObjectReal + */ +trait LazyProxyTrait +{ + /** + * @param \Closure():object $initializer Returns the proxied object + */ + public static function createLazyProxy(\Closure $initializer): static + { + if (self::class !== $class = static::class) { + $skippedProperties = ["\0".self::class."\0lazyObjectId" => true]; + } elseif (\defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) { + Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES; + } + + $instance = (Registry::$classReflectors[$class] ??= new \ReflectionClass($class))->newInstanceWithoutConstructor(); + $instance->lazyObjectId = $id = spl_object_id($instance); + Registry::$states[$id] = new LazyObjectState($initializer); + + foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) { + $reset($instance, $skippedProperties ??= []); + } + + return $instance; + } + + /** + * Forces initialization of a lazy object and returns it. + */ + public function initializeLazyObject(): parent + { + if (isset($this->lazyObjectReal)) { + return $this->lazyObjectReal; + } + + return $this; + } + + /** + * @return bool Returns false when the object cannot be reset, ie when it's not a lazy object + */ + public function resetLazyObject(): bool + { + if (0 >= ($this->lazyObjectId ?? 0)) { + return false; + } + + if (\array_key_exists("\0".self::class."\0lazyObjectReal", (array) $this)) { + unset($this->lazyObjectReal); + } + + return true; + } + + public function &__get($name): mixed + { + $propertyScopes = Hydrator::$propertyScopes[static::class] ??= Hydrator::getPropertyScopes(static::class); + $scope = null; + $instance = $this; + + if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScope($propertyScopes, $class, $name); + + if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) { + if (isset($this->lazyObjectId)) { + if ('lazyObjectReal' === $name && self::class === $scope) { + $this->lazyObjectReal = (Registry::$states[$this->lazyObjectId]->initializer)(); + + return $this->lazyObjectReal; + } + if (isset($this->lazyObjectReal)) { + $instance = $this->lazyObjectReal; + } + } + $parent = 2; + goto get_in_scope; + } + } + $parent = (Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['get']; + + if (isset($this->lazyObjectReal)) { + $instance = $this->lazyObjectReal; + } else { + if (2 === $parent) { + return parent::__get($name); + } + $value = parent::__get($name); + + return $value; + } + + if (!$parent && null === $class && !\array_key_exists($name, (array) $instance)) { + $frame = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; + trigger_error(sprintf('Undefined property: %s::$%s in %s on line %s', $instance::class, $name, $frame['file'], $frame['line']), \E_USER_NOTICE); + } + + get_in_scope: + + if (null === $scope) { + if (null === $readonlyScope && 1 !== $parent) { + return $instance->$name; + } + $value = $instance->$name; + + return $value; + } + $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); + + return $accessor['get']($instance, $name, null !== $readonlyScope || 1 === $parent); + } + + public function __set($name, $value): void + { + $propertyScopes = Hydrator::$propertyScopes[static::class] ??= Hydrator::getPropertyScopes(static::class); + $scope = null; + $instance = $this; + + if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + + if ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { + if (isset($this->lazyObjectId)) { + if ('lazyObjectReal' === $name && self::class === $scope) { + $this->lazyObjectReal = $value; + + return; + } + if (isset($this->lazyObjectReal)) { + $instance = $this->lazyObjectReal; + } + } + goto set_in_scope; + } + } + if (isset($this->lazyObjectReal)) { + $instance = $this->lazyObjectReal; + } elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['set']) { + parent::__set($name, $value); + + return; + } + + set_in_scope: + + if (null === $scope) { + $instance->$name = $value; + + return; + } + $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); + + $accessor['set']($instance, $name, $value); + } + + public function __isset($name): bool + { + $propertyScopes = Hydrator::$propertyScopes[static::class] ??= Hydrator::getPropertyScopes(static::class); + $scope = null; + $instance = $this; + + if ([$class] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScope($propertyScopes, $class, $name); + + if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) { + if (isset($this->lazyObjectId)) { + if ('lazyObjectReal' === $name && self::class === $scope) { + return null !== $this->lazyObjectReal = (Registry::$states[$this->lazyObjectId]->initializer)(); + } + if (isset($this->lazyObjectReal)) { + $instance = $this->lazyObjectReal; + } + } + goto isset_in_scope; + } + } + if (isset($this->lazyObjectReal)) { + $instance = $this->lazyObjectReal; + } elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['isset']) { + return parent::__isset($name); + } + + isset_in_scope: + + if (null === $scope) { + return isset($instance->$name); + } + $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); + + return $accessor['isset']($instance, $name); + } + + public function __unset($name): void + { + $propertyScopes = Hydrator::$propertyScopes[static::class] ??= Hydrator::getPropertyScopes(static::class); + $scope = null; + $instance = $this; + + if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + + if ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { + if (isset($this->lazyObjectId)) { + if ('lazyObjectReal' === $name && self::class === $scope) { + unset($this->lazyObjectReal); + + return; + } + if (isset($this->lazyObjectReal)) { + $instance = $this->lazyObjectReal; + } + } + goto unset_in_scope; + } + } + if (isset($this->lazyObjectReal)) { + $instance = $this->lazyObjectReal; + } elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['unset']) { + parent::__unset($name); + + return; + } + + unset_in_scope: + + if (null === $scope) { + unset($instance->$name); + + return; + } + $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); + + $accessor['unset']($instance, $name); + } + + public function __clone(): void + { + if (!isset($this->lazyObjectId)) { + if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['clone']) { + parent::__clone(); + } + + return; + } + + if (\array_key_exists("\0".self::class."\0lazyObjectReal", (array) $this)) { + $this->lazyObjectReal = clone $this->lazyObjectReal; + } + if ($state = Registry::$states[$this->lazyObjectId] ?? null) { + Registry::$states[$this->lazyObjectId = spl_object_id($this)] = clone $state; + } + } + + public function __serialize(): array + { + $class = self::class; + + if (!isset($this->lazyObjectReal) && (Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['serialize']) { + $properties = parent::__serialize(); + } else { + $properties = (array) $this; + } + unset($properties["\0$class\0lazyObjectId"]); + + if (isset($this->lazyObjectReal) || Registry::$parentMethods[$class]['serialize'] || !Registry::$parentMethods[$class]['sleep']) { + return $properties; + } + + $scope = get_parent_class($class); + $data = []; + + foreach (parent::__sleep() as $name) { + $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null; + + if (null === $k) { + trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $name), \E_USER_NOTICE); + } else { + $data[$k] = $value; + } + } + + return $data; + } + + public function __unserialize(array $data): void + { + $class = self::class; + + if (isset($data["\0$class\0lazyObjectReal"])) { + foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) { + $reset($this, $data); + } + + if (1 < \count($data)) { + PublicHydrator::hydrate($this, $data); + } else { + $this->lazyObjectReal = $data["\0$class\0lazyObjectReal"]; + } + Registry::$states[-1] ??= new LazyObjectState(static fn () => throw new \LogicException('Lazy proxy has no initializer.')); + $this->lazyObjectId = -1; + } elseif ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['unserialize']) { + parent::__unserialize($data); + } else { + PublicHydrator::hydrate($this, $data); + + if (Registry::$parentMethods[$class]['wakeup']) { + parent:__wakeup(); + } + } + } + + public function __destruct() + { + if (isset($this->lazyObjectId)) { + if (0 < $this->lazyObjectId) { + unset(Registry::$states[$this->lazyObjectId]); + } + + return; + } + + if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['destruct']) { + parent::__destruct(); + } + } +} diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php new file mode 100644 index 0000000000000..0aa6c5647a4ee --- /dev/null +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -0,0 +1,351 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter; + +use Symfony\Component\VarExporter\Exception\LogicException; +use Symfony\Component\VarExporter\Internal\Hydrator; +use Symfony\Component\VarExporter\Internal\LazyObjectRegistry; + +/** + * @author Nicolas Grekas + */ +final class ProxyHelper +{ + /** + * Helps generate lazy-loading ghost objects. + * + * @throws LogicException When the class is incompatible with ghost objects + */ + public static function generateLazyGhost(\ReflectionClass $class): string + { + if ($class->isFinal()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name)); + } + if ($class->isInterface() || $class->isAbstract()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name)); + } + if (\stdClass::class !== $class->name && $class->isInternal()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is internal.', $class->name)); + } + if ($class->hasMethod('__get') && 'mixed' !== (self::exportType($class->getMethod('__get')) ?? 'mixed')) { + throw new LogicException(sprintf('Cannot generate lazy ghost: return type of method "%s::__get()" should be "mixed".', $class->name)); + } + + static $traitMethods; + $traitMethods ??= (new \ReflectionClass(LazyGhostTrait::class))->getMethods(); + + foreach ($traitMethods as $method) { + if ($class->hasMethod($method->name) && $class->getMethod($method->name)->isFinal()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: method "%s::%s()" is final.', $class->name, $method->name)); + } + } + + $parent = $class; + while ($parent = $parent->getParentClass()) { + if (\stdClass::class !== $parent->name && $parent->isInternal()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.', $class->name, $parent->name)); + } + } + $propertyScopes = self::exportPropertyScopes($class->name); + $readonly = \PHP_VERSION_ID >= 80200 && $class->isReadOnly() ? 'readonly ' : ''; + + return <<name} implements \Symfony\Component\VarExporter\LazyObjectInterface + { + use \Symfony\Component\VarExporter\LazyGhostTrait; + + private {$readonly}int \$lazyObjectId; + + private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes}; + } + + // Help opcache.preload discover always-needed symbols + class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); + class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); + class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); + + EOPHP; + } + + /** + * Helps generate lazy-loading virtual proxies. + * + * @param \ReflectionClass[] $interfaces + * + * @throws LogicException When the class is incompatible with virtual proxies + */ + public static function generateLazyProxy(?\ReflectionClass $class, array $interfaces = []): string + { + if (!class_exists($class?->name ?? \stdClass::class, false)) { + throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not a class.', $class->name)); + } + if ($class?->isFinal()) { + throw new LogicException(sprintf('Cannot generate lazy proxy: class "%s" is final.', $class->name)); + } + + $methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []]; + foreach ($interfaces as $interface) { + if (!$interface->isInterface()) { + throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name)); + } + $methodReflectors[] = $interface->getMethods(); + } + $methodReflectors = array_merge(...$methodReflectors); + + $extendsInternalClass = false; + if ($parent = $class) { + do { + $extendsInternalClass = \stdClass::class !== $parent->name && $parent->isInternal(); + } while (!$extendsInternalClass && $parent = $parent->getParentClass()); + } + $methodsHaveToBeProxied = $extendsInternalClass; + $methods = []; + + foreach ($methodReflectors as $method) { + if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) { + continue; + } + $methodsHaveToBeProxied = true; + $trait = new \ReflectionMethod(LazyProxyTrait::class, '__get'); + $body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine()); + $body[0] = str_replace('): mixed', '): '.$type, $body[0]); + $methods['__get'] = strtr(implode('', $body).' }', [ + 'Hydrator' => '\\'.Hydrator::class, + 'Registry' => '\\'.LazyObjectRegistry::class, + ]); + break; + } + + foreach ($methodReflectors as $method) { + if ($method->isStatic() || isset($methods[$lcName = strtolower($method->name)])) { + continue; + } + if ($method->isFinal()) { + if ($extendsInternalClass || $methodsHaveToBeProxied || method_exists(LazyProxyTrait::class, $method->name)) { + throw new LogicException(sprintf('Cannot generate lazy proxy: method "%s::%s()" is final.', $class->name, $method->name)); + } + continue; + } + if (method_exists(LazyProxyTrait::class, $method->name) || ($method->isProtected() && !$method->isAbstract())) { + continue; + } + + $signature = self::exportSignature($method); + $parentCall = $method->isAbstract() ? "throw new \BadMethodCallException('Cannot forward abstract method \"{$method->class}::{$method->name}()\".')" : "parent::{$method->name}(...\\func_get_args())"; + + if (str_ends_with($signature, '): never') || str_ends_with($signature, '): void')) { + $body = <<lazyObjectReal)) { + \$this->lazyObjectReal->{$method->name}(...\\func_get_args()); + } else { + {$parentCall}; + } + EOPHP; + } else { + if (!$methodsHaveToBeProxied && !$method->isAbstract()) { + // Skip proxying methods that might return $this + foreach (preg_split('/[()|&]++/', self::exportType($method) ?? 'static') as $type) { + if (\in_array($type = ltrim($type, '?'), ['static', 'object'], true)) { + continue 2; + } + foreach ([$class, ...$interfaces] as $r) { + if ($r && is_a($r->name, $type, true)) { + continue 3; + } + } + } + } + + $body = <<lazyObjectReal)) { + return \$this->lazyObjectReal->{$method->name}(...\\func_get_args()); + } + + return {$parentCall}; + EOPHP; + } + $methods[$lcName] = " {$signature}\n {\n{$body}\n }"; + } + + $readonly = \PHP_VERSION_ID >= 80200 && $class && $class->isReadOnly() ? 'readonly ' : ''; + $types = $interfaces = array_unique(array_column($interfaces, 'name')); + $interfaces[] = LazyObjectInterface::class; + $interfaces = implode(', \\', $interfaces); + $parent = $class ? ' extends \\'.$class->name : ''; + array_unshift($types, $class ? 'parent' : ''); + $type = ltrim(implode('&\\', $types), '&'); + + if (!$class) { + $trait = new \ReflectionMethod(LazyProxyTrait::class, 'initializeLazyObject'); + $body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine()); + $body[0] = str_replace('): parent', '): '.$type, $body[0]); + $methods = ['initializeLazyObject' => implode('', $body).' }'] + $methods; + } + $body = $methods ? "\n".implode("\n\n", $methods)."\n" : ''; + + if ($class) { + $propertyScopes = substr(self::exportPropertyScopes($class->name), 1, -6); + $body = << [self::class, 'lazyObjectReal', null], + "\\0".self::class."\\0lazyObjectReal" => [self::class, 'lazyObjectReal', null],{$propertyScopes} + ]; + {$body} + EOPHP; + } + + return <<getParameters() as $param) { + if (\in_array($default = rtrim(substr(explode('$'.$param->name.' = ', (string) $param, 2)[1] ?? '', 0, -2)), ['', 'NULL'], true)) { + $default = 'null'; + } elseif (str_contains($default, '\\') || str_contains($default, '::') || str_contains($default, '(')) { + $default = self::fixDefault($default, $function); + } + $parameters[] = ($param->getAttributes(\SensitiveParameter::class) ? '#[\SensitiveParameter] ' : '') + .($withParameterTypes && $param->hasType() ? self::exportType($param).' ' : '') + .($param->isPassedByReference() ? '&' : '') + .($param->isVariadic() ? '...' : '').'$'.$param->name + .($param->isOptional() && !$param->isVariadic() ? ' = '.$default : ''); + } + + $signature = 'function '.($function->returnsReference() ? '&' : '') + .($function->isClosure() ? '' : $function->name).'('.implode(', ', $parameters).')'; + + if ($function instanceof \ReflectionMethod) { + $signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' : 'private ')) + .($function->isStatic() ? 'static ' : '').$signature; + } + if ($function->hasReturnType()) { + $signature .= ': '.self::exportType($function); + } + + static $getPrototype; + $getPrototype ??= (new \ReflectionMethod(\ReflectionMethod::class, 'getPrototype'))->invoke(...); + + while ($function) { + if ($function->hasTentativeReturnType()) { + return '#[\ReturnTypeWillChange] '.$signature; + } + + try { + $function = $function instanceof \ReflectionMethod && $function->isAbstract() ? false : $getPrototype($function); + } catch (\ReflectionException) { + break; + } + } + + return $signature; + } + + public static function exportType(\ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionParameter $owner, bool $noBuiltin = false, \ReflectionType $type = null): ?string + { + if (!$type ??= $owner instanceof \ReflectionFunctionAbstract ? $owner->getReturnType() : $owner->getType()) { + return null; + } + $class = null; + $types = []; + if ($type instanceof \ReflectionUnionType) { + $reflectionTypes = $type->getTypes(); + $glue = '|'; + } elseif ($type instanceof \ReflectionIntersectionType) { + $reflectionTypes = $type->getTypes(); + $glue = '&'; + } else { + $reflectionTypes = [$type]; + $glue = null; + } + + foreach ($reflectionTypes as $type) { + if ($type instanceof \ReflectionIntersectionType) { + if ('' !== $name = '('.self::exportType($owner, $noBuiltin, $type).')') { + $types[] = $name; + } + continue; + } + $name = $type->getName(); + + if ($noBuiltin && $type->isBuiltin()) { + continue; + } + if (\in_array($name, ['parent', 'self'], true) && $class ??= $owner->getDeclaringClass()) { + $name = 'parent' === $name ? ($class->getParentClass() ?: null)?->name ?? 'parent' : $class->name; + } + + $types[] = ($noBuiltin || $type->isBuiltin() || 'static' === $name ? '' : '\\').$name; + } + + if (!$types) { + return ''; + } + if (null === $glue) { + return (!$noBuiltin && $type->allowsNull() && 'mixed' !== $name ? '?' : '').$types[0]; + } + sort($types); + + return implode($glue, $types); + } + + private static function exportPropertyScopes(string $parent): string + { + $propertyScopes = Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent); + uksort($propertyScopes, 'strnatcmp'); + $propertyScopes = VarExporter::export($propertyScopes); + $propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes); + $propertyScopes = preg_replace("/(?|(,)\n( ) |\n |,\n (\]))/", '$1$2', $propertyScopes); + $propertyScopes = str_replace("\n", "\n ", $propertyScopes); + + return $propertyScopes; + } + + private static function fixDefault(string $default, \ReflectionFunctionAbstract $function): string + { + $regexp = "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/"; + $parts = preg_split($regexp, $default, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); + + $regexp = '/([\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(?!: )/'; + $callback = $function instanceof \ReflectionMethod + ? fn ($m) => $m[1].match ($m[2]) { + 'new' => 'new', + 'self' => '\\'.$function->getDeclaringClass()->name, + 'namespace\\parent', + 'parent' => ($parent = $function->getDeclaringClass()->getParentClass()) ? '\\'.$parent->name : 'parent', + default => '\\'.$m[2], + } + : fn ($m) => $m[1].(\in_array($m[2], ['new', 'self', 'parent'], true) ? '' : '\\').$m[2]; + + return implode('', array_map(fn ($part) => match ($part[0]) { + '"' => $part, // for internal classes only + "'" => str_replace("\0", '\'."\0".\'', $part), + default => preg_replace_callback($regexp, $callback, $part), + }, $parts)); + } +} diff --git a/src/Symfony/Component/VarExporter/README.md b/src/Symfony/Component/VarExporter/README.md index a2e2a9050f1cb..bc0ed193613ef 100644 --- a/src/Symfony/Component/VarExporter/README.md +++ b/src/Symfony/Component/VarExporter/README.md @@ -11,7 +11,7 @@ of objects: - `Instantiator::instantiate()` creates an object and sets its properties without calling its constructor nor any other methods. - `Hydrator::hydrate()` can set the properties of an existing object. -- `LazyGhostObjectTrait` can make a class behave as a lazy loading ghost object. +- `Lazy*Trait` can make a class behave as a lazy-loading ghost or virtual proxy. VarExporter::export() --------------------- @@ -57,34 +57,61 @@ Hydrator::hydrate($object, [], [ ]); ``` -LazyGhostObjectTrait --------------------- +`Lazy*Trait` +------------ -By using `LazyGhostObjectTrait` either directly in your classes or using -inheritance, you can make their instances able to lazy load themselves. This -works by creating these instances empty and by computing their state only when -accessing a property. +The component provides two lazy-loading patterns: ghost objects and virtual +proxies (see https://martinfowler.com/eaaCatalog/lazyLoad.html for reference.) + +Ghost objects work only with concrete and non-internal classes. In the generic +case, they are not compatible with using factories in their initializer. + +Virtual proxies work with concrete, abstract or internal classes. They provide an +API that looks like the actual objects and forward calls to them. They can cause +identity problems because proxies might not be seen as equivalents to the actual +objects they proxy. + +Because of this identity problem, ghost objects should be preferred when +possible. Exceptions thrown by the `ProxyHelper` class can help decide when it +can be used or not. + +Ghost objects and virtual proxies both provide implementations for the +`LazyObjectInterface` which allows resetting them to their initial state or to +forcibly initialize them when needed. Note that resetting a ghost object skips +its read-only properties. You should use a virtual proxy to reset read-only +properties. + +### `LazyGhostTrait` + +By using `LazyGhostTrait` either directly in your classes or by using +`ProxyHelper::generateLazyGhost()`, you can make their instances lazy-loadable. +This works by creating these instances empty and by computing their state only +when accessing a property. ```php -FooMadeLazy extends Foo +class FooLazyGhost extends Foo { - use LazyGhostObjectTrait; + use LazyGhostTrait; + + private int $lazyObjectId; } -// This closure will be called when the object needs to be initialized, ie when a property is accessed -$initializer = function (Foo $instance) { - // [...] Use whatever heavy logic you need here to compute the $dependencies of the $instance +$foo = FooLazyGhost::createLazyGhost(initializer: function (Foo $instance): void { + // [...] Use whatever heavy logic you need here + // to compute the $dependencies of the $instance $instance->__construct(...$dependencies); -}; + // [...] Call setters, etc. if needed +}); -$foo = FooMadeLazy::createLazyGhostObject($initializer); +// $foo is now a lazy-loading ghost object. The initializer will +// be called only when and if a *property* is accessed. ``` You can also partially initialize the objects on a property-by-property basis by adding two arguments to the initializer: ```php -$initializer = function (Foo $instance, string $propertyName, ?string $propertyScope) { +$initializer = function (Foo $instance, string $propertyName, ?string $propertyScope): mixed { if (Foo::class === $propertyScope && 'bar' === $propertyName) { return 123; } @@ -96,6 +123,28 @@ Because lazy-initialization is not triggered when (un)setting a property, it's also possible to do partial initialization by calling setters on a just-created ghost object. +### `LazyProxyTrait` + +Alternatively, `LazyProxyTrait` can be used to create virtual proxies: + +```php +$proxyCode = ProxyHelper::generateLazyProxy(new ReflectionClass(Foo::class)); +// $proxyCode contains the reference to LazyProxyTrait +// and should be dumped into a file in production envs +eval('class FooLazyProxy'.$proxyCode); + +$foo = FooLazyProxy::createLazyProxy(initializer: function (): Foo { + // [...] Use whatever heavy logic you need here + // to compute the $dependencies of the $instance + $instance = new Foo(...$dependencies); + // [...] Call setters, etc. if needed + + return $instance; +}); +// $foo is now a lazy-loading virtual proxy object. The initializer will +// be called only when and if a *method* is called. +``` + Resources --------- diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildMagicClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php similarity index 59% rename from src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildMagicClass.php rename to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php index b9926ab78e93a..a7dc5d3d73fb4 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildMagicClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php @@ -9,14 +9,15 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject; +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost; -use Symfony\Component\VarExporter\LazyGhostObjectInterface; -use Symfony\Component\VarExporter\LazyGhostObjectTrait; +use Symfony\Component\VarExporter\LazyGhostTrait; +use Symfony\Component\VarExporter\LazyObjectInterface; -class ChildMagicClass extends MagicClass implements LazyGhostObjectInterface +class ChildMagicClass extends MagicClass implements LazyObjectInterface { - use LazyGhostObjectTrait; + use LazyGhostTrait; + private int $lazyObjectId; private int $data = 123; } diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildStdClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildStdClass.php similarity index 57% rename from src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildStdClass.php rename to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildStdClass.php index c9e7f10085e26..0a131d9bfb5c1 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildStdClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildStdClass.php @@ -9,12 +9,14 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject; +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost; -use Symfony\Component\VarExporter\LazyGhostObjectInterface; -use Symfony\Component\VarExporter\LazyGhostObjectTrait; +use Symfony\Component\VarExporter\LazyGhostTrait; +use Symfony\Component\VarExporter\LazyObjectInterface; -class ChildStdClass extends \stdClass implements LazyGhostObjectInterface +class ChildStdClass extends \stdClass implements LazyObjectInterface { - use LazyGhostObjectTrait; + use LazyGhostTrait; + + private int $lazyObjectId; } diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildTestClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildTestClass.php similarity index 87% rename from src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildTestClass.php rename to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildTestClass.php index f971ae452f29e..ea5b8bfb26c3d 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/ChildTestClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildTestClass.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject; +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost; -use Symfony\Component\VarExporter\LazyGhostObjectInterface; +use Symfony\Component\VarExporter\LazyObjectInterface; -class ChildTestClass extends TestClass implements LazyGhostObjectInterface +class ChildTestClass extends TestClass implements LazyObjectInterface { public int $public = 4; public readonly int $publicReadonly; diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/MagicClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/MagicClass.php similarity index 99% rename from src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/MagicClass.php rename to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/MagicClass.php index 204ccc09242a7..6ac1f7364b517 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/MagicClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/MagicClass.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject; +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost; class MagicClass { diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/NoMagicClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/NoMagicClass.php similarity index 99% rename from src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/NoMagicClass.php rename to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/NoMagicClass.php index 75cea5a6dfb57..88fa4f0dbd64b 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/NoMagicClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/NoMagicClass.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject; +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost; class NoMagicClass { diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/TestClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/TestClass.php similarity index 86% rename from src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/TestClass.php rename to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/TestClass.php index ecb4d4236f5a2..1c1127d546399 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/TestClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/TestClass.php @@ -9,14 +9,15 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject; +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost; -use Symfony\Component\VarExporter\LazyGhostObjectTrait; +use Symfony\Component\VarExporter\LazyGhostTrait; class TestClass extends NoMagicClass { - use LazyGhostObjectTrait; + use LazyGhostTrait; + private int $lazyObjectId; public int $public = 1; protected int $protected = 2; protected readonly int $protectedReadonly; diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/FinalPublicClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/FinalPublicClass.php new file mode 100644 index 0000000000000..e61e2ee782520 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/FinalPublicClass.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +class FinalPublicClass +{ + private $count = 0; + + final public function increment(): int + { + return $this->count += 1; + } + + public function decrement(): int + { + return $this->count -= 1; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/ReadOnlyClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/ReadOnlyClass.php new file mode 100644 index 0000000000000..45221614a7101 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/ReadOnlyClass.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +readonly class ReadOnlyClass +{ + public function __construct( + public int $foo + ) { + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/StringMagicGetClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/StringMagicGetClass.php new file mode 100644 index 0000000000000..9f79b0b077a01 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/StringMagicGetClass.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +class StringMagicGetClass +{ + public function __get(string $name): string + { + return $name; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestClass.php new file mode 100644 index 0000000000000..0f12c6db2c660 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestClass.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +#[\AllowDynamicProperties] +class TestClass +{ + public function __construct( + protected \stdClass $dep, + ) { + } + + public function getDep(): \stdClass + { + return $this->dep; + } + + public function __destruct() + { + $this->dep->destructed = true; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestUnserializeClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestUnserializeClass.php new file mode 100644 index 0000000000000..f9e2e2475f17c --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestUnserializeClass.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +class TestUnserializeClass extends TestClass +{ + public function __serialize(): array + { + return [$this->dep]; + } + + public function __unserialize(array $data): void + { + $this->dep = $data[0]; + $this->dep->wokeUp = true; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestWakeupClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestWakeupClass.php new file mode 100644 index 0000000000000..f473e53f66fab --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestWakeupClass.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +class TestWakeupClass extends TestClass +{ + public function __wakeup() + { + $this->dep->wokeUp = true; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostObjectTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php similarity index 55% rename from src/Symfony/Component/VarExporter/Tests/LazyGhostObjectTraitTest.php rename to src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index b6fc527a779f1..346974519d45f 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostObjectTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -12,45 +12,44 @@ namespace Symfony\Component\VarExporter\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Component\VarExporter\Internal\GhostObjectId; -use Symfony\Component\VarExporter\Internal\GhostObjectRegistry; -use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject\ChildMagicClass; -use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject\ChildStdClass; -use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject\ChildTestClass; -use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject\MagicClass; -use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhostObject\TestClass; - -class LazyGhostObjectTraitTest extends TestCase +use Symfony\Component\VarExporter\Internal\LazyObjectRegistry; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildMagicClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildStdClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildTestClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\MagicClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\TestClass; + +class LazyGhostTraitTest extends TestCase { public function testGetPublic() { - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $ghost) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $ghost) { $ghost->__construct(); }); - $this->assertSame(["\0".TestClass::class."\0lazyGhostObjectId"], array_keys((array) $instance)); + $this->assertSame(["\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); $this->assertSame(-4, $instance->public); $this->assertSame(4, $instance->publicReadonly); } public function testIssetPublic() { - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $ghost) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $ghost) { $ghost->__construct(); }); - $this->assertSame(["\0".TestClass::class."\0lazyGhostObjectId"], array_keys((array) $instance)); + $this->assertSame(["\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); $this->assertTrue(isset($instance->public)); $this->assertSame(4, $instance->publicReadonly); } public function testUnsetPublic() { - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $ghost) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $ghost) { $ghost->__construct(); }); - $this->assertSame(["\0".TestClass::class."\0lazyGhostObjectId"], array_keys((array) $instance)); + $this->assertSame(["\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); unset($instance->public); $this->assertFalse(isset($instance->public)); $this->assertSame(4, $instance->publicReadonly); @@ -58,11 +57,11 @@ public function testUnsetPublic() public function testSetPublic() { - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $ghost) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $ghost) { $ghost->__construct(); }); - $this->assertSame(["\0".TestClass::class."\0lazyGhostObjectId"], array_keys((array) $instance)); + $this->assertSame(["\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); $instance->public = 12; $this->assertSame(12, $instance->public); $this->assertSame(4, $instance->publicReadonly); @@ -70,32 +69,35 @@ public function testSetPublic() public function testClone() { - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $ghost) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $ghost) { $ghost->__construct(); }); $clone = clone $instance; $this->assertNotSame((array) $instance, (array) $clone); - $this->assertSame(["\0".TestClass::class."\0lazyGhostObjectId"], array_keys((array) $instance)); - $this->assertSame(["\0".TestClass::class."\0lazyGhostObjectId"], array_keys((array) $clone)); + $this->assertSame(["\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); + $this->assertSame(["\0".TestClass::class."\0lazyObjectId"], array_keys((array) $clone)); $clone = clone $clone; - $this->assertTrue($clone->resetLazyGhostObject()); + $this->assertTrue($clone->resetLazyObject()); } public function testSerialize() { - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $ghost) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $ghost) { $ghost->__construct(); }); $serialized = serialize($instance); - $this->assertStringNotContainsString('lazyGhostObjectId', $serialized); + $this->assertStringNotContainsString('lazyObjectId', $serialized); $clone = unserialize($serialized); - $this->assertSame(array_keys((array) $instance), array_keys((array) $clone)); - $this->assertFalse($clone->resetLazyGhostObject()); + $expected = (array) $instance; + $this->assertArrayHasKey("\0".TestClass::class."\0lazyObjectId", $expected); + unset($expected["\0".TestClass::class."\0lazyObjectId"]); + $this->assertSame(array_keys($expected), array_keys((array) $clone)); + $this->assertFalse($clone->resetLazyObject()); } /** @@ -124,49 +126,49 @@ public function provideMagicClass() { yield [new MagicClass()]; - yield [ChildMagicClass::createLazyGhostObject(function (ChildMagicClass $instance) { + yield [ChildMagicClass::createLazyGhost(function (ChildMagicClass $instance) { $instance->__construct(); })]; } public function testDestruct() { - $registryCount = \count(GhostObjectRegistry::$states); + $registryCount = \count(LazyObjectRegistry::$states); $destructCounter = MagicClass::$destructCounter; - $instance = ChildMagicClass::createLazyGhostObject(function (ChildMagicClass $instance) { + $instance = ChildMagicClass::createLazyGhost(function (ChildMagicClass $instance) { $instance->__construct(); }); unset($instance); $this->assertSame($destructCounter, MagicClass::$destructCounter); - $instance = ChildMagicClass::createLazyGhostObject(function (ChildMagicClass $instance) { + $instance = ChildMagicClass::createLazyGhost(function (ChildMagicClass $instance) { $instance->__construct(); }); - $instance->initializeLazyGhostObject(); + $instance->initializeLazyObject(); unset($instance); $this->assertSame(1 + $destructCounter, MagicClass::$destructCounter); - $this->assertCount($registryCount, GhostObjectRegistry::$states); + $this->assertCount($registryCount, LazyObjectRegistry::$states); } - public function testResetLazyGhostObject() + public function testResetLazyGhost() { - $instance = ChildMagicClass::createLazyGhostObject(function (ChildMagicClass $instance) { + $instance = ChildMagicClass::createLazyGhost(function (ChildMagicClass $instance) { $instance->__construct(); }); $instance->foo = 234; - $this->assertTrue($instance->resetLazyGhostObject()); + $this->assertTrue($instance->resetLazyObject()); $this->assertSame('bar', $instance->foo); } public function testFullInitialization() { $counter = 0; - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $ghost) use (&$counter) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $ghost) use (&$counter) { ++$counter; $ghost->__construct(); }); @@ -180,7 +182,7 @@ public function testFullInitialization() public function testPartialInitialization() { $counter = 0; - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $instance, string $propertyName, ?string $propertyScope) use (&$counter) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $instance, string $propertyName, ?string $propertyScope) use (&$counter) { ++$counter; return match ($propertyName) { @@ -195,25 +197,25 @@ public function testPartialInitialization() }; }); - $this->assertSame(["\0".TestClass::class."\0lazyGhostObjectId"], array_keys((array) $instance)); + $this->assertSame(["\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); $this->assertSame(123, $instance->public); - $this->assertSame(['public', "\0".TestClass::class."\0lazyGhostObjectId"], array_keys((array) $instance)); + $this->assertSame(["\0".TestClass::class."\0lazyObjectId", 'public'], array_keys((array) $instance)); $this->assertSame(1, $counter); - $instance->initializeLazyGhostObject(); + $instance->initializeLazyObject(); $this->assertSame(123, $instance->public); $this->assertSame(6, $counter); $properties = (array) $instance; + $this->assertIsInt($properties["\0".TestClass::class."\0lazyObjectId"]); + unset($properties["\0".TestClass::class."\0lazyObjectId"]); $this->assertSame(array_keys((array) new ChildTestClass()), array_keys($properties)); - $properties = array_values($properties); - $this->assertInstanceOf(GhostObjectId::class, array_splice($properties, 4, 1)[0]); $this->assertSame([123, 345, 456, 567, 234, 678], array_values($properties)); } public function testPartialInitializationWithReset() { - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $instance, string $propertyName, ?string $propertyScope) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $instance, string $propertyName, ?string $propertyScope) { return 234; }); @@ -222,26 +224,26 @@ public function testPartialInitializationWithReset() $this->assertSame(234, $instance->publicReadonly); $this->assertSame(123, $instance->public); - $this->assertTrue($instance->resetLazyGhostObject()); + $this->assertTrue($instance->resetLazyObject()); $this->assertSame(234, $instance->publicReadonly); $this->assertSame(123, $instance->public); - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $instance, string $propertyName, ?string $propertyScope) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $instance, string $propertyName, ?string $propertyScope) { return 234; }); - $instance->resetLazyGhostObject(); + $instance->resetLazyObject(); $instance->public = 123; $this->assertSame(123, $instance->public); - $this->assertTrue($instance->resetLazyGhostObject()); + $this->assertTrue($instance->resetLazyObject()); $this->assertSame(234, $instance->public); } public function testPartialInitializationWithNastyPassByRef() { - $instance = ChildTestClass::createLazyGhostObject(function (ChildTestClass $instance, string &$propertyName, ?string &$propertyScope) { + $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $instance, string &$propertyName, ?string &$propertyScope) { return $propertyName = $propertyScope = 123; }); @@ -250,7 +252,7 @@ public function testPartialInitializationWithNastyPassByRef() public function testSetStdClassProperty() { - $instance = ChildStdClass::createLazyGhostObject(function (ChildStdClass $ghost) { + $instance = ChildStdClass::createLazyGhost(function (ChildStdClass $ghost) { }); $instance->public = 12; diff --git a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php new file mode 100644 index 0000000000000..4926aa0cc4324 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php @@ -0,0 +1,262 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\TestClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\TestUnserializeClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\TestWakeupClass; + +class LazyProxyTraitTest extends TestCase +{ + public function testGetter() + { + $initCounter = 0; + $proxy = $this->createLazyProxy(TestClass::class, function () use (&$initCounter) { + ++$initCounter; + + return new TestClass((object) ['hello' => 'world']); + }); + + $this->assertInstanceOf(TestClass::class, $proxy); + $this->assertSame(0, $initCounter); + + $dep1 = $proxy->getDep(); + $this->assertSame(1, $initCounter); + + $this->assertTrue($proxy->resetLazyObject()); + $this->assertSame(1, $initCounter); + + $dep2 = $proxy->getDep(); + $this->assertSame(2, $initCounter); + $this->assertNotSame($dep1, $dep2); + } + + public function testInitialize() + { + $initCounter = 0; + $proxy = $this->createLazyProxy(TestClass::class, function () use (&$initCounter) { + ++$initCounter; + + return new TestClass((object) ['hello' => 'world']); + }); + + $this->assertSame(0, $initCounter); + + $proxy->initializeLazyObject(); + $this->assertSame(1, $initCounter); + + $proxy->initializeLazyObject(); + $this->assertSame(1, $initCounter); + } + + public function testClone() + { + $initCounter = 0; + $proxy = $this->createLazyProxy(TestClass::class, function () use (&$initCounter) { + ++$initCounter; + + return new TestClass((object) ['hello' => 'world']); + }); + + $clone = clone $proxy; + $this->assertSame(0, $initCounter); + + $dep1 = $proxy->getDep(); + $this->assertSame(1, $initCounter); + + $dep2 = $clone->getDep(); + $this->assertSame(2, $initCounter); + + $this->assertNotSame($dep1, $dep2); + } + + public function testUnserialize() + { + $initCounter = 0; + $proxy = $this->createLazyProxy(TestUnserializeClass::class, function () use (&$initCounter) { + ++$initCounter; + + return new TestUnserializeClass((object) ['hello' => 'world']); + }); + + $this->assertInstanceOf(TestUnserializeClass::class, $proxy); + $this->assertSame(0, $initCounter); + + $copy = unserialize(serialize($proxy)); + $this->assertSame(1, $initCounter); + + $this->assertFalse($copy->resetLazyObject()); + $this->assertTrue($copy->getDep()->wokeUp); + $this->assertSame('world', $copy->getDep()->hello); + } + + public function testWakeup() + { + $initCounter = 0; + $proxy = $this->createLazyProxy(TestWakeupClass::class, function () use (&$initCounter) { + ++$initCounter; + + return new TestWakeupClass((object) ['hello' => 'world']); + }); + + $this->assertInstanceOf(TestWakeupClass::class, $proxy); + $this->assertSame(0, $initCounter); + + $copy = unserialize(serialize($proxy)); + $this->assertSame(1, $initCounter); + + $this->assertFalse($copy->resetLazyObject()); + $this->assertTrue($copy->getDep()->wokeUp); + $this->assertSame('world', $copy->getDep()->hello); + } + + public function testDestruct() + { + $initCounter = 0; + $proxy = $this->createLazyProxy(TestClass::class, function () use (&$initCounter) { + ++$initCounter; + + return new TestClass((object) ['hello' => 'world']); + }); + + unset($proxy); + $this->assertSame(0, $initCounter); + + $proxy = $this->createLazyProxy(TestClass::class, function () use (&$initCounter) { + ++$initCounter; + + return new TestClass((object) ['hello' => 'world']); + }); + $dep = $proxy->getDep(); + $this->assertSame(1, $initCounter); + unset($proxy); + $this->assertTrue($dep->destructed); + } + + public function testDynamicProperty() + { + $initCounter = 0; + $proxy = $this->createLazyProxy(TestClass::class, function () use (&$initCounter) { + ++$initCounter; + + return new TestClass((object) ['hello' => 'world']); + }); + + $proxy->dynProp = 123; + $this->assertSame(1, $initCounter); + $this->assertSame(123, $proxy->dynProp); + $this->assertTrue(isset($proxy->dynProp)); + $this->assertCount(2, (array) $proxy); + unset($proxy->dynProp); + $this->assertFalse(isset($proxy->dynProp)); + $this->assertCount(2, (array) $proxy); + } + + public function testStringMagicGet() + { + $proxy = $this->createLazyProxy(StringMagicGetClass::class, function () { + return new StringMagicGetClass(); + }); + + $this->assertSame('abc', $proxy->abc); + } + + public function testFinalPublicClass() + { + $proxy = $this->createLazyProxy(FinalPublicClass::class, function () { + return new FinalPublicClass(); + }); + + $this->assertSame(1, $proxy->increment()); + $this->assertSame(2, $proxy->increment()); + $this->assertSame(1, $proxy->decrement()); + } + + public function testWither() + { + $obj = new class() { + public $foo = 123; + + public function withFoo($foo): static + { + $clone = clone $this; + $clone->foo = $foo; + + return $clone; + } + }; + $proxy = $this->createLazyProxy($obj::class, fn () => $obj); + + $clone = $proxy->withFoo(234); + $this->assertSame($clone::class, $proxy::class); + $this->assertSame(234, $clone->foo); + $this->assertSame(234, $obj->foo); + } + + public function testFluent() + { + $obj = new class() { + public $foo = 123; + + public function setFoo($foo): static + { + $this->foo = $foo; + + return $this; + } + }; + $proxy = $this->createLazyProxy($obj::class, fn () => $obj); + + $this->assertSame($proxy->setFoo(234), $proxy); + $this->assertSame(234, $proxy->foo); + } + + /** + * @requires PHP 8.2 + */ + public function testReadOnlyClass() + { + $proxy = $this->createLazyProxy(ReadOnlyClass::class, fn () => new ReadOnlyClass(123)); + + $this->assertSame(123, $proxy->foo); + } + + /** + * @template T + * + * @param class-string $class + * + * @return T + */ + private function createLazyProxy(string $class, \Closure $initializer): object + { + $r = new \ReflectionClass($class); + + if (str_contains($class, "\0")) { + $class = __CLASS__.'\\'.debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'].'_L'.$r->getStartLine(); + class_alias($r->name, $class); + } + $proxy = str_replace($r->name, $class, ProxyHelper::generateLazyProxy($r)); + $class = str_replace('\\', '_', $class).'_'.md5($proxy); + + if (!class_exists($class, false)) { + eval((\PHP_VERSION_ID >= 80200 && $r->isReadOnly() ? 'readonly ' : '').'class '.$class.' '.$proxy); + } + + return $class::createLazyProxy($initializer); + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php new file mode 100644 index 0000000000000..8d489bb7812b1 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php @@ -0,0 +1,253 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\VarExporter\Exception\LogicException; +use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass; + +class ProxyHelperTest extends TestCase +{ + /** + * @dataProvider provideExportSignature + */ + public function testExportSignature(string $expected, \ReflectionMethod $method) + { + $this->assertSame($expected, ProxyHelper::exportSignature($method)); + } + + public function provideExportSignature() + { + $methods = (new \ReflectionClass(TestForProxyHelper::class))->getMethods(); + $source = file(__FILE__); + + foreach ($methods as $method) { + $expected = substr($source[$method->getStartLine() - 1], $method->isAbstract() ? 13 : 4, -(1 + $method->isAbstract())); + $expected = str_replace(['.', ' . . . ', '"', '\0'], [' . ', '...', "'", '\'."\0".\''], $expected); + $expected = str_replace('Bar', '\\'.Bar::class, $expected); + $expected = str_replace('self', '\\'.TestForProxyHelper::class, $expected); + + yield [$expected, $method]; + } + } + + public function testExportSignatureFQ() + { + $expected = <<<'EOPHP' + public function bar($a = \Symfony\Component\VarExporter\Tests\Bar::BAZ, + $b = new \Symfony\Component\VarExporter\Tests\Bar(\Symfony\Component\VarExporter\Tests\Bar::BAZ, bar: \Symfony\Component\VarExporter\Tests\Bar::BAZ), + $c = new \stdClass(), + $d = new \Symfony\Component\VarExporter\Tests\TestSignatureFQ(), + $e = new \Symfony\Component\VarExporter\Tests\Bar(), + $f = new \Symfony\Component\VarExporter\Tests\Qux(), + $g = new \Symfony\Component\VarExporter\Tests\Qux(), + $i = new \Qux(), + $j = \stdClass::BAZ, + $k = \Symfony\Component\VarExporter\Tests\Bar) + EOPHP; + + $this->assertSame($expected, str_replace(', $', ",\n$", ProxyHelper::exportSignature(new \ReflectionMethod(TestSignatureFQ::class, 'bar')))); + } + + public function testGenerateLazyProxy() + { + $expected = <<<'EOPHP' + extends \Symfony\Component\VarExporter\Tests\TestForProxyHelper implements \Symfony\Component\VarExporter\LazyObjectInterface + { + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private int $lazyObjectId; + private parent $lazyObjectReal; + + private const LAZY_OBJECT_PROPERTY_SCOPES = [ + 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], + "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null], + ]; + + public function foo1(): ?\Symfony\Component\VarExporter\Tests\Bar + { + if (isset($this->lazyObjectReal)) { + return $this->lazyObjectReal->foo1(...\func_get_args()); + } + + return parent::foo1(...\func_get_args()); + } + + public function foo4(\Symfony\Component\VarExporter\Tests\Bar|string $b): void + { + if (isset($this->lazyObjectReal)) { + $this->lazyObjectReal->foo4(...\func_get_args()); + } else { + parent::foo4(...\func_get_args()); + } + } + + protected function foo7() + { + if (isset($this->lazyObjectReal)) { + return $this->lazyObjectReal->foo7(...\func_get_args()); + } + + return throw new \BadMethodCallException('Cannot forward abstract method "Symfony\Component\VarExporter\Tests\TestForProxyHelper::foo7()".'); + } + } + + // Help opcache.preload discover always-needed symbols + class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); + class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); + class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); + + EOPHP; + + $this->assertSame($expected, ProxyHelper::generateLazyProxy(new \ReflectionClass(TestForProxyHelper::class))); + } + + public function testGenerateLazyProxyForInterfaces() + { + $expected = <<<'EOPHP' + implements \Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface1, \Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface2, \Symfony\Component\VarExporter\LazyObjectInterface + { + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private int $lazyObjectId; + private \Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface1&\Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface2 $lazyObjectReal; + + public function initializeLazyObject(): \Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface1&\Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface2 + { + if (isset($this->lazyObjectReal)) { + return $this->lazyObjectReal; + } + + return $this; + } + + public function foo1(): ?\Symfony\Component\VarExporter\Tests\Bar + { + if (isset($this->lazyObjectReal)) { + return $this->lazyObjectReal->foo1(...\func_get_args()); + } + + return throw new \BadMethodCallException('Cannot forward abstract method "Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface1::foo1()".'); + } + + public function foo2(?\Symfony\Component\VarExporter\Tests\Bar $b): \Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface2 + { + if (isset($this->lazyObjectReal)) { + return $this->lazyObjectReal->foo2(...\func_get_args()); + } + + return throw new \BadMethodCallException('Cannot forward abstract method "Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface2::foo2()".'); + } + } + + // Help opcache.preload discover always-needed symbols + class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); + class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); + class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); + + EOPHP; + + $this->assertSame($expected, ProxyHelper::generateLazyProxy(null, [new \ReflectionClass(TestForProxyHelperInterface1::class), new \ReflectionClass(TestForProxyHelperInterface2::class)])); + } + + public function testAttributes() + { + $expected = <<<'EOPHP' + + public function foo(#[\SensitiveParameter] $a): int + { + if (isset($this->lazyObjectReal)) { + return $this->lazyObjectReal->foo(...\func_get_args()); + } + + return parent::foo(...\func_get_args()); + } + } + + EOPHP; + + $class = new \ReflectionClass(new class() { + #[SomeAttribute] + public function foo(#[\SensitiveParameter, AnotherAttribute] $a): int + { + } + }); + $this->assertStringContainsString($expected, ProxyHelper::generateLazyProxy($class)); + } + + public function testCannotGenerateGhostForStringMagicGet() + { + $this->expectException(LogicException::class); + ProxyHelper::generateLazyGhost(new \ReflectionClass(StringMagicGetClass::class)); + } +} + +abstract class TestForProxyHelper +{ + public function foo1(): ?Bar + { + } + + public function foo2(?Bar $b): ?self + { + } + + public function &foo3(Bar &$b, string &...$c) + { + } + + public function foo4(Bar|string $b): void + { + } + + public function foo5($b = new \stdClass([0 => 123]).Bar.Bar::BAR."a\0b") + { + } + + protected function foo6($b = null): never + { + } + + abstract protected function foo7(); + + public static function foo8() + { + } +} + +interface TestForProxyHelperInterface1 +{ + public function foo1(): ?Bar; +} + +interface TestForProxyHelperInterface2 +{ + public function foo2(?Bar $b): self; +} + +class TestSignatureFQ extends \stdClass +{ + public function bar( + $a = Bar::BAZ, + $b = new Bar(Bar::BAZ, bar: Bar::BAZ), + $c = new parent(), + $d = new self(), + $e = new namespace\Bar(), + $f = new Qux(), + $g = new namespace\Qux(), + $i = new \Qux(), + $j = parent::BAZ, + $k = Bar, + ) { + } +} diff --git a/src/Symfony/Component/VarExporter/VarExporter.php b/src/Symfony/Component/VarExporter/VarExporter.php index 3e2a4cc038631..c820fc902d554 100644 --- a/src/Symfony/Component/VarExporter/VarExporter.php +++ b/src/Symfony/Component/VarExporter/VarExporter.php @@ -33,7 +33,7 @@ final class VarExporter * Exports a serializable PHP value to PHP code. * * @param bool &$isStaticValue Set to true after execution if the provided value is static, false otherwise - * @param bool &$classes Classes found in the value are added to this list as both keys and values + * @param bool &$foundClasses Classes found in the value are added to this list as both keys and values * * @throws ExceptionInterface When the provided value cannot be serialized */ diff --git a/src/Symfony/Component/VarExporter/composer.json b/src/Symfony/Component/VarExporter/composer.json index 3aac3aff326e7..67c4a279f4c7d 100644 --- a/src/Symfony/Component/VarExporter/composer.json +++ b/src/Symfony/Component/VarExporter/composer.json @@ -2,7 +2,7 @@ "name": "symfony/var-exporter", "type": "library", "description": "Allows exporting any serializable PHP data structure to plain PHP code", - "keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone"], + "keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone", "lazy loading", "proxy"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy