From 48621393478d15f05ba6f1d2fc029b5bf76159d9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 3 Aug 2022 18:02:10 +0200 Subject: [PATCH] [DependencyInjection][VarExporter] Generate lazy proxies for non-ghostable lazy services out of the box --- .github/expected-missing-return-types.diff | 46 +-- .../Bridge/Doctrine/ManagerRegistry.php | 6 +- .../Tests/LazyProxy/ContainerBuilderTest.php | 2 + .../Tests/LazyProxy/Dumper/PhpDumperTest.php | 2 + .../DependencyInjection/CHANGELOG.md | 2 +- .../Compiler/AutowirePass.php | 8 +- .../Compiler/ResolveBindingsPass.php | 7 +- .../Compiler/ResolveNamedArgumentsPass.php | 4 +- .../DependencyInjection/ContainerBuilder.php | 9 - .../DependencyInjection/Dumper/PhpDumper.php | 26 +- .../Instantiator/LazyServiceInstantiator.php | 14 +- .../LazyProxy/PhpDumper/LazyServiceDumper.php | 118 +++--- .../LazyProxy/ProxyHelper.php | 4 +- .../Tests/ContainerBuilderTest.php | 3 +- .../Tests/Dumper/PhpDumperTest.php | 2 +- .../php/services9_lazy_inlined_factories.txt | 19 +- .../php/services_dedup_lazy_ghost.php | 34 +- .../php/services_dedup_lazy_proxy.php | 13 - .../Fixtures/php/services_non_shared_lazy.php | 13 - .../php/services_non_shared_lazy_as_files.txt | 34 +- .../php/services_non_shared_lazy_ghost.php | 30 +- .../Fixtures/php/services_wither_lazy.php | 39 +- .../ErrorHandler/DebugClassLoader.php | 4 +- ...RegisterControllerArgumentLocatorsPass.php | 6 +- .../Component/VarExporter/CHANGELOG.md | 2 +- .../LogicException.php} | 7 +- .../VarExporter/Internal/GhostObjectId.php | 39 -- ...ectRegistry.php => LazyObjectRegistry.php} | 59 ++- ...ostObjectState.php => LazyObjectState.php} | 42 ++- ...hostObjectTrait.php => LazyGhostTrait.php} | 156 ++++---- ...tInterface.php => LazyObjectInterface.php} | 10 +- .../Component/VarExporter/LazyProxyTrait.php | 345 +++++++++++++++++ .../Component/VarExporter/ProxyHelper.php | 351 ++++++++++++++++++ src/Symfony/Component/VarExporter/README.md | 79 +++- .../ChildMagicClass.php | 11 +- .../ChildStdClass.php | 12 +- .../ChildTestClass.php | 6 +- .../MagicClass.php | 2 +- .../NoMagicClass.php | 2 +- .../TestClass.php | 7 +- .../Fixtures/LazyProxy/FinalPublicClass.php | 27 ++ .../Fixtures/LazyProxy/ReadOnlyClass.php | 20 + .../LazyProxy/StringMagicGetClass.php | 20 + .../Tests/Fixtures/LazyProxy/TestClass.php | 31 ++ .../LazyProxy/TestUnserializeClass.php | 26 ++ .../Fixtures/LazyProxy/TestWakeupClass.php | 20 + ...ctTraitTest.php => LazyGhostTraitTest.php} | 98 ++--- .../VarExporter/Tests/LazyProxyTraitTest.php | 262 +++++++++++++ .../VarExporter/Tests/ProxyHelperTest.php | 253 +++++++++++++ .../Component/VarExporter/VarExporter.php | 2 +- .../Component/VarExporter/composer.json | 2 +- 51 files changed, 1818 insertions(+), 518 deletions(-) rename src/Symfony/Component/VarExporter/{Internal/EmptyScope.php => Exception/LogicException.php} (65%) delete mode 100644 src/Symfony/Component/VarExporter/Internal/GhostObjectId.php rename src/Symfony/Component/VarExporter/Internal/{GhostObjectRegistry.php => LazyObjectRegistry.php} (60%) rename src/Symfony/Component/VarExporter/Internal/{GhostObjectState.php => LazyObjectState.php} (59%) rename src/Symfony/Component/VarExporter/{LazyGhostObjectTrait.php => LazyGhostTrait.php} (63%) rename src/Symfony/Component/VarExporter/{LazyGhostObjectInterface.php => LazyObjectInterface.php} (62%) create mode 100644 src/Symfony/Component/VarExporter/LazyProxyTrait.php create mode 100644 src/Symfony/Component/VarExporter/ProxyHelper.php rename src/Symfony/Component/VarExporter/Tests/Fixtures/{LazyGhostObject => LazyGhost}/ChildMagicClass.php (59%) rename src/Symfony/Component/VarExporter/Tests/Fixtures/{LazyGhostObject => LazyGhost}/ChildStdClass.php (57%) rename src/Symfony/Component/VarExporter/Tests/Fixtures/{LazyGhostObject => LazyGhost}/ChildTestClass.php (87%) rename src/Symfony/Component/VarExporter/Tests/Fixtures/{LazyGhostObject => LazyGhost}/MagicClass.php (99%) rename src/Symfony/Component/VarExporter/Tests/Fixtures/{LazyGhostObject => LazyGhost}/NoMagicClass.php (99%) rename src/Symfony/Component/VarExporter/Tests/Fixtures/{LazyGhostObject => LazyGhost}/TestClass.php (86%) create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/FinalPublicClass.php create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/ReadOnlyClass.php create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/StringMagicGetClass.php create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestClass.php create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestUnserializeClass.php create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/TestWakeupClass.php rename src/Symfony/Component/VarExporter/Tests/{LazyGhostObjectTraitTest.php => LazyGhostTraitTest.php} (55%) create mode 100644 src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php create mode 100644 src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php 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": [