diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php index 6c219ba83b..199d4a6c07 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php @@ -19,49 +19,15 @@ * @Flow\Proxy(false) * @api */ -class DependencyProxy +interface DependencyProxy { - /** - * @var string - */ - protected $className; - - /** - * @var \Closure - */ - protected $builder; - - /** - * @var array - */ - protected $propertyVariables = []; - - /** - * Constructs this proxy - * - * @param string $className Implementation class name of the dependency to proxy - * @param \Closure $builder The closure which eventually builds the dependency - */ - public function __construct($className, \Closure $builder) - { - $this->className = $className; - $this->builder = $builder; - } - /** * Activate the dependency and set it in the object. * * @return object The real dependency object * @api */ - public function _activateDependency() - { - $realDependency = $this->builder->__invoke(); - foreach ($this->propertyVariables as &$propertyVariable) { - $propertyVariable = $realDependency; - } - return $realDependency; - } + public function _activateDependency(); /** * Returns the class name of the proxied dependency @@ -69,10 +35,7 @@ public function _activateDependency() * @return string Fully qualified class name of the proxied object * @api */ - public function _getClassName() - { - return $this->className; - } + public function _getClassName(); /** * Adds another variable by reference where the actual dependency object should @@ -81,21 +44,5 @@ public function _getClassName() * @param mixed &$propertyVariable The variable to replace * @return void */ - public function _addPropertyVariable(&$propertyVariable) - { - $this->propertyVariables[] = &$propertyVariable; - } - - /** - * Proxy magic call method which triggers the injection of the real dependency - * and returns the result of a call to the original method in the dependency - * - * @param string $methodName Name of the method to be called - * @param array $arguments An array of arguments to be passed to the method - * @return mixed - */ - public function __call($methodName, array $arguments) - { - return $this->_activateDependency()->$methodName(...$arguments); - } + public function _addPropertyVariable(&$propertyVariable); } diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxyTrait.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxyTrait.php new file mode 100644 index 0000000000..603abd8987 --- /dev/null +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxyTrait.php @@ -0,0 +1,90 @@ +_dependencyClassName = $className; + $instance->_dependencyBuilder = $builder; + return $instance; + } + + /** + * Activate the dependency and set it in the object. + * + * @return object The real dependency object + * @api + */ + public function _activateDependency() + { + $realDependency = $this->_dependencyBuilder->__invoke(); + foreach ($this->_dependencyPropertyVariables as &$propertyVariable) { + $propertyVariable = $realDependency; + } + return $realDependency; + } + + /** + * Returns the class name of the proxied dependency + * + * @return string Fully qualified class name of the proxied object + * @api + */ + public function _getClassName(): string + { + return $this->_dependencyClassName; + } + + /** + * Adds another variable by reference where the actual dependency object should + * be injected into once this proxy is activated. + * + * @param mixed &$propertyVariable The variable to replace + * @return void + */ + public function _addPropertyVariable(&$propertyVariable): void + { + $this->_dependencyPropertyVariables[] = &$propertyVariable; + } +} diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php index 70f0f29c24..d5b15fcdd0 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php @@ -22,6 +22,7 @@ use Neos\Flow\ObjectManagement\Proxy\Compiler; use Neos\Flow\ObjectManagement\Proxy\ObjectSerializationTrait; use Neos\Flow\ObjectManagement\Proxy\ProxyClass; +use Neos\Flow\Reflection\ClassReflection; use Neos\Flow\Reflection\MethodReflection; use Neos\Flow\Reflection\ReflectionService; use Neos\Utility\Arrays; @@ -299,15 +300,15 @@ protected function buildConstructorInjectionCode(Configuration $objectConfigurat } $assignments[$argumentPosition] = $assignmentPrologue . '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $argumentValue . '\')'; } - break; + break; case ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE: $assignments[$argumentPosition] = $assignmentPrologue . var_export($argumentValue, true); - break; + break; case ConfigurationArgument::ARGUMENT_TYPES_SETTING: $assignments[$argumentPosition] = $assignmentPrologue . '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\Flow\Configuration\ConfigurationManager::class)->getConfiguration(\Neos\Flow\Configuration\ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, \'' . $argumentValue . '\')'; - break; + break; } } } @@ -360,7 +361,7 @@ protected function buildPropertyInjectionCode(Configuration $objectConfiguration $commands = array_merge($commands, $this->buildPropertyInjectionCodeByString($objectConfiguration, $propertyConfiguration, $propertyName, $propertyValue)); } - break; + break; case ConfigurationProperty::PROPERTY_TYPES_STRAIGHTVALUE: if (is_string($propertyValue)) { $preparedSetterArgument = '\'' . str_replace('\'', '\\\'', $propertyValue) . '\''; @@ -372,14 +373,14 @@ protected function buildPropertyInjectionCode(Configuration $objectConfiguration $preparedSetterArgument = $propertyValue; } $commands[] = 'if (\Neos\Utility\ObjectAccess::setProperty($this, \'' . $propertyName . '\', ' . $preparedSetterArgument . ') === false) { $this->' . $propertyName . ' = ' . $preparedSetterArgument . ';}'; - break; + break; case ConfigurationProperty::PROPERTY_TYPES_CONFIGURATION: $configurationType = $propertyValue['type']; if (!in_array($configurationType, $this->configurationManager->getAvailableConfigurationTypes())) { throw new ObjectException\UnknownObjectException('The configuration injection specified for property "' . $propertyName . '" in the object configuration of object "' . $objectConfiguration->getObjectName() . '" refers to the unknown configuration type "' . $configurationType . '".', 1420736211); } $commands = array_merge($commands, $this->buildPropertyInjectionCodeByConfigurationTypeAndPath($objectConfiguration, $propertyName, $configurationType, $propertyValue['path'])); - break; + break; } $injectedProperties[] = $propertyName; } @@ -406,7 +407,6 @@ protected function buildPropertyInjectionCode(Configuration $objectConfiguration protected function buildPropertyInjectionCodeByConfiguration(Configuration $objectConfiguration, $propertyName, Configuration $propertyConfiguration) { $className = $objectConfiguration->getClassName(); - $propertyObjectName = $propertyConfiguration->getObjectName(); $propertyClassName = $propertyConfiguration->getClassName(); if ($propertyClassName === null) { $preparedSetterArgument = $this->buildCustomFactoryCall($propertyConfiguration->getFactoryObjectName(), $propertyConfiguration->getFactoryMethodName(), $propertyConfiguration->getFactoryArguments()); @@ -427,7 +427,9 @@ protected function buildPropertyInjectionCodeByConfiguration(Configuration $obje return $result; } - return $this->buildLazyPropertyInjectionCode($propertyObjectName, $propertyClassName, $propertyName, $preparedSetterArgument); + // It's hard to predict what we are going to inject due to what can be configured via Objects.yaml, + // so we don't allow lazy injection in this case: + return [' $this->' . $propertyName . ' = ' . $preparedSetterArgument . ';']; } /** @@ -466,7 +468,34 @@ public function buildPropertyInjectionCodeByString(Configuration $objectConfigur return $result; } - if ($propertyConfiguration->isLazyLoading() && $this->objectConfigurations[$propertyObjectName]->getScope() !== Configuration::SCOPE_PROTOTYPE) { + // FIXME – just for testing + $propertyClassHasInterfaceWithConstructor = false; + $propertyClassHasFinalMethod = false; + if ($propertyClassName) { + foreach ((new ClassReflection($propertyClassName))->getInterfaceNames() as $interfaceName) { + if (method_exists($interfaceName, '__construct')) { + $propertyClassHasInterfaceWithConstructor = true; + break; + } + } + foreach (get_class_methods($propertyClassName) as $methodName) { + if ($this->reflectionService->isMethodFinal($propertyClassName, $methodName)) { + $propertyClassHasFinalMethod = true; + } + } + } + + if ( + $propertyConfiguration->isLazyLoading() && + $this->objectConfigurations[$propertyObjectName]->getScope() !== Configuration::SCOPE_PROTOTYPE && + !interface_exists($this->objectConfigurations[$propertyObjectName]->getClassName()) && + !$this->objectConfigurations[$propertyObjectName]->isCreatedByFactory() && + !$propertyClassHasInterfaceWithConstructor && + !$propertyClassHasFinalMethod && + $this->compiler->getProxyClass($propertyClassName) !== false + + // FIXME MORE + ) { return $this->buildLazyPropertyInjectionCode($propertyObjectName, $propertyClassName, $propertyName, $preparedSetterArgument); } else { return [' $this->' . $propertyName . ' = ' . $preparedSetterArgument . ';']; @@ -633,15 +662,15 @@ protected function buildMethodParametersCode(array $argumentConfigurations) } $preparedArguments[] = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $argumentValue . '\')'; } - break; + break; case ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE: $preparedArguments[] = var_export($argumentValue, true); - break; + break; case ConfigurationArgument::ARGUMENT_TYPES_SETTING: $preparedArguments[] = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\Flow\Configuration\ConfigurationManager::class)->getConfiguration(\Neos\Flow\Configuration\ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, \'' . $argumentValue . '\')'; - break; + break; } } } diff --git a/Neos.Flow/Classes/ObjectManagement/ObjectManager.php b/Neos.Flow/Classes/ObjectManagement/ObjectManager.php index 0f84732aa4..b9082d372e 100644 --- a/Neos.Flow/Classes/ObjectManagement/ObjectManager.php +++ b/Neos.Flow/Classes/ObjectManagement/ObjectManager.php @@ -406,16 +406,21 @@ public function getLazyDependencyByHash($hash, &$propertyReferenceVariable) * @param string &$propertyReferenceVariable A first variable where the dependency needs to be injected into * @param string $className Name of the class of the dependency which eventually will be instantiated * @param \Closure $builder An anonymous function which creates the instance to be injected - * @return DependencyProxy + * @return DependencyProxy | null */ - public function createLazyDependency($hash, &$propertyReferenceVariable, $className, \Closure $builder): DependencyProxy + public function createLazyDependency($hash, &$propertyReferenceVariable, $className, \Closure $builder): ?DependencyProxy { - $this->dependencyProxies[$hash] = new DependencyProxy($className, $builder); + // Trigger auto-loading of the original class, because any generated lazy proxy will be + // contained in the same file and the auto-loader does not know how to find a lazy proxy: + class_exists($className) || interface_exists($className); + + $lazyProxyClassName = $className . '_LazyProxy'; + $this->dependencyProxies[$hash] = call_user_func_array([$lazyProxyClassName, '_createDependencyProxy'], [$className, $builder]); $this->dependencyProxies[$hash]->_addPropertyVariable($propertyReferenceVariable); + return $this->dependencyProxies[$hash]; } - /** * Unsets the instance of the given object * diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php index a77b09cc3c..4846658aee 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php @@ -11,9 +11,18 @@ * source code. */ +use Laminas\Code\Generator\ClassGenerator; +use Laminas\Code\Generator\MethodGenerator; +use Laminas\Code\Generator\ParameterGenerator; use Neos\Flow\Annotations as Flow; +use Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy; +use Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxyTrait; +use Neos\Flow\ObjectManagement\Exception\CannotBuildObjectException; use Neos\Flow\Reflection\ClassReflection; +use Neos\Flow\Reflection\Exception\ClassLoadingForReflectionFailedException; +use Neos\Flow\Reflection\MethodReflection; use Neos\Flow\Reflection\ReflectionService; +use ReflectionException; /** * Representation of a Proxy Class during rendering time @@ -204,10 +213,12 @@ public function addTraits(array $traitNames) * Renders and returns the PHP code for this ProxyClass. * * @return string + * @throws ReflectionException + * @throws ClassLoadingForReflectionFailedException + * @throws CannotBuildObjectException */ public function render() { - $namespace = $this->namespace; $proxyClassName = $this->originalClassName; $originalClassName = $this->originalClassName . Compiler::ORIGINAL_CLASSNAME_SUFFIX; $classModifier = ''; @@ -235,7 +246,27 @@ public function render() $constantsCode . $propertiesCode . $methodsCode . - '}'; + "}\n\n"; + + $hasInterfaceWithConstructor = false; + foreach ((new ClassReflection($this->fullOriginalClassName))->getInterfaceNames() as $interfaceName) { + if (method_exists($interfaceName, '__construct')) { + $hasInterfaceWithConstructor = true; + break; + } + } + + $hasFinalMethod = false; + foreach (get_class_methods($this->fullOriginalClassName) as $methodName) { + if ($this->reflectionService->isMethodFinal($this->fullOriginalClassName, $methodName)) { + $hasFinalMethod = true; + } + } + + if (!$hasInterfaceWithConstructor && !$hasFinalMethod && !$this->reflectionService->isClassAbstract($this->fullOriginalClassName)) { + $classCode .= $this->buildLazyProxyClass($proxyClassName, $originalClassName); + } + return $classCode; } @@ -302,4 +333,66 @@ protected function renderTraitsCode() return ' use ' . implode(', ', $this->traits) . ";\n\n"; } + + /** + * @param $proxyClassName + * @param string $originalClassName + * @return string + * @throws ReflectionException + */ + protected function buildLazyProxyClass($proxyClassName, string $originalClassName): string + { + $methods = [$this->buildCallMagicMethod()]; + + foreach (get_class_methods($this->fullOriginalClassName) as $methodName) { + if (!$this->reflectionService->isMethodPublic($this->fullOriginalClassName, $methodName) || $methodName === '__call' || $methodName === '__construct') { + continue; + } + $methodReturnType = $this->reflectionService->getMethodDeclaredReturnType($this->fullOriginalClassName, $methodName); + $returnKeyword = ($methodReturnType === 'void') ? '' : 'return '; + $method = MethodGenerator::fromReflection(new \Laminas\Code\Reflection\MethodReflection($this->fullOriginalClassName, $methodName)); + $method->removeDocBlock(); + $method->setBody( + <<< CODE + \$arguments = func_get_args(); + {$returnKeyword}\$this->_activateDependency()->{$methodName}(...\$arguments); + CODE + ); + $methods[] = $method; + } + + return ClassGenerator::fromArray(['name' => $proxyClassName . '_LazyProxy']) + ->setExtendedClass($originalClassName) + ->setImplementedInterfaces([DependencyProxy::class]) + ->addTrait('\\' . DependencyProxyTrait::class) + ->removeMethod('__call') + ->addMethods($methods) + ->generate(); + } + + /** + * @return MethodGenerator + * @throws ReflectionException + */ + protected function buildCallMagicMethod(): MethodGenerator + { + if (method_exists($this->fullOriginalClassName, '__call')) { + $callMagicMethod = MethodGenerator::fromReflection(new \Laminas\Code\Reflection\MethodReflection($this->fullOriginalClassName, '__call')); + } else { + $callMagicMethod = MethodGenerator::fromArray(['name' => '__call']); + $callMagicMethod->setParameters([ + ParameterGenerator::fromArray(['name' => 'methodName', 'type' => 'string']), + ParameterGenerator::fromArray(['name' => 'arguments', 'type' => 'array']), + ]); + } + + $callMagicMethod->setBody( + <<< CODE + [\$methodName, \$arguments] = func_get_args(); + return \$this->_activateDependency()->\$methodName(...\$arguments); + CODE + ); + $callMagicMethod->removeDocBlock(); + return $callMagicMethod; + } } diff --git a/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithLazyDependencies.php b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithLazyDependencies.php index 45545f3388..9a7a871697 100644 --- a/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithLazyDependencies.php +++ b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithLazyDependencies.php @@ -28,7 +28,7 @@ class ClassWithLazyDependencies * @Flow\Inject * @var SingletonClassB */ - public $lazyB; + public ?SingletonClassB $lazyB = null; /** * @Flow\Inject(lazy = false) diff --git a/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SingletonClassBsub.php b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SingletonClassBsub.php new file mode 100644 index 0000000000..afb241fd38 --- /dev/null +++ b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SingletonClassBsub.php @@ -0,0 +1,23 @@ +objectManager->forgetInstance(Fixtures\SingletonClassA::class); - $this->objectManager->forgetInstance(Fixtures\SingletonClassB::class); + $this->objectManager->forgetInstance(Fixtures\SingletonClassBsub::class); $object1 = $this->objectManager->get(Fixtures\ClassWithLazyDependencies::class); $object2 = $this->objectManager->get(Fixtures\AnotherClassWithLazyDependencies::class); diff --git a/Neos.Flow/Tests/Unit/ObjectManagement/DependencyInjection/DependencyProxyTest.php b/Neos.Flow/Tests/Unit/ObjectManagement/DependencyInjection/DependencyProxyTest.php deleted file mode 100644 index 3adeac1f6d..0000000000 --- a/Neos.Flow/Tests/Unit/ObjectManagement/DependencyInjection/DependencyProxyTest.php +++ /dev/null @@ -1,28 +0,0 @@ -_getClassName()); - } -}