diff --git a/Neos.Flow/Classes/Annotations/Proxy.php b/Neos.Flow/Classes/Annotations/Proxy.php index e7e709d811..e5d9edb025 100644 --- a/Neos.Flow/Classes/Annotations/Proxy.php +++ b/Neos.Flow/Classes/Annotations/Proxy.php @@ -12,12 +12,20 @@ */ use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; +use Neos\Flow\ObjectManagement\Exception\ProxyCompilerException; /** - * Used to disable proxy building for an object. + * Controls proxy class generation behavior for a class. * - * If disabled, neither Dependency Injection nor AOP can be used - * on the object. + * This annotation allows you to: + * - Disable proxy building entirely (enabled=false) - useful for value objects, DTOs, + * or classes that should not use Dependency Injection or AOP + * - Force generation of serialization code (forceSerializationCode=true) - rarely needed + * escape hatch for edge cases where automatic detection of entity relationships fails + * + * When proxy building is disabled (enabled=false), neither Dependency Injection nor AOP + * can be used on the object. The class will be instantiated directly without any + * framework enhancements. * * @Annotation * @NamedArgumentConstructor @@ -27,13 +35,64 @@ final class Proxy { /** - * Whether proxy building for the target is disabled. (Can be given as anonymous argument.) - * @var boolean + * Whether proxy building is enabled for this class. + * + * When set to false, Flow will not generate a proxy class, meaning: + * - No Dependency Injection (no Flow\Inject annotations) + * - No Aspect-Oriented Programming (no AOP advices) + * - No automatic serialization handling + * - The class is instantiated directly without any framework enhancements + * + * This is useful for simple value objects, DTOs, or utility classes that don't need + * framework features and where you want to avoid the minimal overhead of proxy classes. + * + * (Can be given as anonymous argument.) */ - public $enabled = true; + public bool $enabled = true; - public function __construct(bool $enabled = true) + /** + * Force the generation of serialization code (__sleep/__wakeup methods) in the proxy class. + * + * Flow automatically detects when serialization code is needed (e.g., when a class has entity + * properties, injected dependencies, or transient properties) and generates the appropriate + * __sleep() and __wakeup() methods. These methods handle: + * - Converting entity references to metadata (class name + persistence identifier) + * - Removing injected and framework-internal properties before serialization + * - Restoring entity references and re-injecting dependencies after deserialization + * + * This flag serves as an **escape hatch for rare edge cases** where automatic detection fails, + * such as: + * - Complex generic/template types that aren't fully parsed (e.g., ComplexType) + * - Deeply nested entity structures where type hints don't reveal the entity relationship + * - Union or intersection types with entities that the reflection system cannot fully analyze + * - Properties with dynamic types where documentation hints are non-standard + * + * IMPORTANT: You should rarely need this flag. Flow's automatic detection handles: + * - Properties typed with Flow\Entity classes + * - Properties with Flow\Inject annotations + * - Properties with Flow\Transient annotations + * - Classes with AOP advices + * - Session-scoped objects + * + * If you find yourself needing this flag for standard entity properties, injected dependencies, + * or other common cases, this indicates a bug in Flow's detection logic that should be reported + * at https://github.com/neos/flow-development-collection/issues + * + * Note: Disabling serialization code (not possible via this flag) would break classes with + * AOP, injections, or entity relationships. To completely opt out of proxy features, use + * enabled=false instead. + * + * @see https://flowframework.readthedocs.io/ for more information on object serialization + */ + public bool $forceSerializationCode = false; + + public function __construct(bool $enabled = true, bool $forceSerializationCode = false) { + if ($enabled === false && $forceSerializationCode === true) { + throw new ProxyCompilerException('Cannot disable a Proxy but forceSerializationCode at the same time.', 1756813222); + } + $this->enabled = $enabled; + $this->forceSerializationCode = $forceSerializationCode; } } diff --git a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php index 757b0d72b6..17f30d3906 100644 --- a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php @@ -424,6 +424,7 @@ public function buildProxyClass(string $targetClassName, array $aspectContainers PHP); } $proxyClass->addTraits(['\\' . AdvicesTrait::class]); + $proxyClass->addInterfaces(['\\' . Aop\ProxyInterface::class]); $this->buildMethodsInterceptorCode($targetClassName, $interceptedMethods); diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php b/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php index bb24986041..ca6a94eb60 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php @@ -160,7 +160,7 @@ public function setClassName($className) /** * Returns the class name * - * @return string Name of the implementing class of this object + * @return class-string Name of the implementing class of this object */ public function getClassName() { diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php index 62165d5fcf..7b341d1843 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php @@ -12,6 +12,7 @@ */ use Neos\Flow\Annotations as Flow; +use Neos\Flow\Aop\ProxyInterface as AopProxyInterface; use Neos\Flow\Cache\CacheManager; use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException; @@ -31,6 +32,7 @@ use Neos\Flow\Reflection\MethodReflection; use Neos\Flow\Reflection\ReflectionService; use Neos\Utility\Arrays; +use Neos\Utility\TypeHandling; use Psr\Log\LoggerInterface; /** @@ -113,31 +115,24 @@ public function build(): void } $this->logger->debug(sprintf('Building dependency injection proxy for "%s"', $className), LogEnvironment::fromMethodName(__METHOD__)); - $constructor = $proxyClass->getConstructor(); + $constructorInjectionCode = $this->buildConstructorInjectionCode($objectConfiguration); + $injectionCodeWasIntroduced = $constructorInjectionCode !== ''; + + $constructor = $proxyClass->getConstructor(withOriginalArgumentSignature: $injectionCodeWasIntroduced === false); $constructor->addPreParentCallCode($this->buildSetInstanceCode($objectConfiguration)); - $constructor->addPreParentCallCode($this->buildConstructorInjectionCode($objectConfiguration)); - $sleepMethod = $proxyClass->getMethod('__sleep'); - $sleepMethod->setDocBlock(self::AUTOGENERATED_PROXY_METHOD_COMMENT); - $sleepMethod->setReturnType('array'); + $constructor->addPreParentCallCode($constructorInjectionCode); $wakeupMethod = $proxyClass->getMethod('__wakeup'); $wakeupMethod->setDocBlock(self::AUTOGENERATED_PROXY_METHOD_COMMENT); $wakeupMethod->addPreParentCallCode($this->buildSetInstanceCode($objectConfiguration)); - - $serializeRelatedEntitiesCode = $this->buildSerializeRelatedEntitiesCode($objectConfiguration); - if ($serializeRelatedEntitiesCode !== '') { - $proxyClass->addTraits(['\\' . ObjectSerializationTrait::class]); - $sleepMethod->addPostParentCallCode($serializeRelatedEntitiesCode); - $wakeupMethod->addPreParentCallCode($this->buildSetRelatedEntitiesCode()); - } - $wakeupMethod->addPostParentCallCode($this->buildLifecycleInitializationCode($objectConfiguration, ObjectManagerInterface::INITIALIZATIONCAUSE_RECREATED)); $wakeupMethod->addPostParentCallCode($this->buildLifecycleShutdownCode($objectConfiguration, ObjectManagerInterface::INITIALIZATIONCAUSE_RECREATED)); $injectPropertiesCode = $this->buildPropertyInjectionCode($objectConfiguration); if ($injectPropertiesCode !== '') { + $injectionCodeWasIntroduced = true; $proxyClass->addTraits(['\\' . PropertyInjectionTrait::class]); $injectPropertiesMethod = $proxyClass->getMethod('Flow_Proxy_injectProperties'); $injectPropertiesMethod->addPreParentCallCode($injectPropertiesCode); @@ -158,6 +153,25 @@ public function build(): void ); } + $couldHaveEntityRelations = $this->couldHaveEntityRelations($objectConfiguration); + $isProxyWithAop = in_array('\\' . AopProxyInterface::class, $proxyClass->getInterfaces()); + $serializeRelatedEntitiesCode = $this->buildSerializeRelatedEntitiesCode($objectConfiguration, $isProxyWithAop || $injectionCodeWasIntroduced); + if ($serializeRelatedEntitiesCode !== '') { + $proxyClass->addTraits(['\\' . ObjectSerializationTrait::class]); + if ($couldHaveEntityRelations) { + $proxyClass->addProperty('Flow_Persistence_RelatedEntitiesContainer', null, 'private readonly \Neos\Flow\ObjectManagement\Proxy\RelatedEntitiesContainer'); + $constructor->addPostParentCallCode('$this->Flow_Persistence_RelatedEntitiesContainer = new \Neos\Flow\ObjectManagement\Proxy\RelatedEntitiesContainer();'); + } + + $classHasSleepMethod = $this->reflectionService->hasMethod($className, '__sleep'); + if (!$classHasSleepMethod) { + $sleepMethod = $proxyClass->getMethod('__sleep'); + $sleepMethod->setDocBlock(self::AUTOGENERATED_PROXY_METHOD_COMMENT); + $sleepMethod->addPostParentCallCode($serializeRelatedEntitiesCode); + } + $wakeupMethod->addPreParentCallCode($this->buildSetRelatedEntitiesCode()); + } + $constructor->addPostParentCallCode($this->buildLifecycleInitializationCode($objectConfiguration, ObjectManagerInterface::INITIALIZATIONCAUSE_CREATED)); $constructor->addPostParentCallCode($this->buildLifecycleShutdownCode($objectConfiguration, ObjectManagerInterface::INITIALIZATIONCAUSE_CREATED)); @@ -208,14 +222,10 @@ protected function buildSetInstanceCode(Configuration $objectConfiguration): str * NOTE: Even though the method name suggests that it is only dealing with related entities code, it is currently also * used for removing injected properties before serialization. This should be refactored in the future. */ - protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfiguration): string + protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfiguration, bool $forceSerializationCode): string { $className = $objectConfiguration->getClassName(); - - if ($this->reflectionService->hasMethod($className, '__sleep')) { - return ''; - } - + $forceSerializationCode = $forceSerializationCode === false ? ($this->reflectionService->getClassAnnotation($className, Flow\Proxy::class)?->forceSerializationCode ?? false) : true; $scopeAnnotation = $this->reflectionService->getClassAnnotation($className, Flow\Scope::class); $transientProperties = $this->reflectionService->getPropertyNamesByAnnotation($className, Flow\Transient::class); $injectedProperties = $this->reflectionService->getPropertyNamesByAnnotation($className, Flow\Inject::class); @@ -225,7 +235,7 @@ protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfig $doBuildCode = $doBuildCode || (count($injectedProperties) > 0); $doBuildCode = $doBuildCode || ($scopeAnnotation->value ?? 'prototype') === 'session'; - if ($doBuildCode === false) { + if (!$forceSerializationCode && $doBuildCode === false) { return ''; } @@ -235,7 +245,7 @@ protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfig $propertyVarTags[$propertyName] = $varTagValues[0] ?? null; } - if (count($transientProperties) === 0 && count($propertyVarTags) === 0) { + if (!$forceSerializationCode && count($transientProperties) === 0 && count($propertyVarTags) === 0) { return ''; } @@ -246,8 +256,6 @@ protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfig var_export($propertyVarTags, true) ], <<<'PHP' - $this->Flow_Object_PropertiesToSerialize = []; - $this->Flow_Persistence_RelatedEntities = null; $result = $this->Flow_serializeRelatedEntities({{transientPropertiesArrayCode}}, {{propertyVarTagsArrayCode}}); PHP ); @@ -255,7 +263,7 @@ protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfig protected function buildSetRelatedEntitiesCode(): string { - return "\n " . '$this->Flow_setRelatedEntities();' . "\n"; + return "\n" . '$this->Flow_setRelatedEntities();' . "\n"; } /** @@ -313,9 +321,20 @@ protected function buildConstructorInjectionCode(Configuration $objectConfigurat $settings = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, array_shift($settingPath)); $argumentValue = Arrays::getValueByPath($settings, $settingPath); } - if (!isset($this->objectConfigurations[$argumentValue])) { + $argumentObjectConfiguration = $this->objectConfigurations[$argumentValue] ?? null; + if ($argumentObjectConfiguration === null) { throw new UnknownObjectException('The object "' . $argumentValue . '" which was specified as an argument in the object configuration of object "' . $objectConfiguration->getObjectName() . '" does not exist.', 1264669967); } + + $constructorAutowiringWithPrototypes = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow.object.dependencyInjection.constructorAutowiringWithPrototypes') ?? true; + if ($constructorAutowiringWithPrototypes === false && $argumentObjectConfiguration->getScope() === Configuration::SCOPE_PROTOTYPE) { + break; + } + + if ($argumentObjectConfiguration->getScope() === Configuration::SCOPE_PROTOTYPE) { + $this->logger->debug(sprintf('Class "%s" will get prototype "%s" injected via constructor, this is deprecated.', $objectConfiguration->getClassName(), $argumentObjectConfiguration->getClassName())); + } + $assignments[$argumentPosition] = $assignmentPrologue . '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $argumentValue . '\')'; $doReturnCode = true; } @@ -340,6 +359,10 @@ protected function buildConstructorInjectionCode(Configuration $objectConfigurat unset($assignments[$argumentCounter]); } + if ($argumentCounter < 0) { + $doReturnCode = false; + } + $code = $argumentCounter >= 0 ? "\n" . implode(";\n", $assignments) . ";\n" : ''; $index = 0; @@ -610,17 +633,17 @@ protected function buildLifecycleInitializationCode(Configuration $objectConfigu return ''; } $className = $objectConfiguration->getClassName(); - $code = "\n" . ' $isSameClass = get_class($this) === \'' . $className . '\';'; + $code = "\n" . '$isSameClass = get_class($this) === \'' . $className . '\';'; if ($cause === ObjectManagerInterface::INITIALIZATIONCAUSE_RECREATED) { - $code .= "\n" . ' $classParents = class_parents($this);'; - $code .= "\n" . ' $classImplements = class_implements($this);'; - $code .= "\n" . ' $isClassProxy = array_search(\'' . $className . '\', $classParents) !== false && array_search(\'Doctrine\Persistence\Proxy\', $classImplements) !== false;' . "\n"; - $code .= "\n" . ' if ($isSameClass || $isClassProxy) {' . "\n"; + $code .= "\n" . '$classParents = class_parents($this);'; + $code .= "\n" . '$classImplements = class_implements($this);'; + $code .= "\n" . '$isClassProxy = array_search(\'' . $className . '\', $classParents) !== false && array_search(\'Doctrine\Persistence\Proxy\', $classImplements) !== false;' . "\n"; + $code .= "\n" . 'if ($isSameClass || $isClassProxy) {' . "\n"; } else { - $code .= "\n" . ' if ($isSameClass) {' . "\n"; + $code .= "\n" . 'if ($isSameClass) {' . "\n"; } - $code .= ' $this->' . $lifecycleInitializationMethodName . '(' . $cause . ');' . "\n"; - $code .= ' }' . "\n"; + $code .= ' $this->' . $lifecycleInitializationMethodName . '(' . $cause . ');' . "\n"; + $code .= '}' . "\n"; return $code; } @@ -739,4 +762,67 @@ protected function compileStaticMethods(string $className, ProxyClass $proxyClas $compiledMethod->setBody('return ' . $compiledResult . ';'); } } + + protected function couldHaveEntityRelations(Configuration $objectConfiguration): bool + { + $result = false; + $className = $objectConfiguration->getClassName(); + $classPropertyNames = $this->reflectionService->getClassPropertyNames($className); + foreach ($classPropertyNames as $propertyName) { + if ( + $this->reflectionService->isPropertyAnnotatedWith($className, $propertyName, Flow\Transient::class) || + $this->reflectionService->isPropertyAnnotatedWith($className, $propertyName, Flow\Inject::class) + ) { + continue; + } + $propertyType = $this->reflectionService->getPropertyType($className, $propertyName); + if ($propertyType === null) { + $propertyType = $this->reflectionService->getPropertyTagValues($className, $propertyName, 'var'); + } + if (isset($propertyType[0])) { + $propertyType = $propertyType[0]; + } + if ($propertyType === null) { + continue; + } + + try { + $typeInformation = TypeHandling::parseType($propertyType); + } catch (\Throwable $throwable) { + // we will skip on unparsable types + continue; + } + if ( + TypeHandling::isSimpleType($typeInformation['type']) || + ($typeInformation['elementType'] !== null && TypeHandling::isSimpleType($typeInformation['elementType'])) + ) { + continue; + } + + if ($typeInformation['type'] === \Closure::class) { + continue; + } + + if (!class_exists($typeInformation['type'])) { + continue; + } + + $isEntity = $this->reflectionService->getClassAnnotation($typeInformation['type'], Flow\Entity::class); + if (!$isEntity) { + continue; + } + + foreach ($objectConfiguration->getProperties() as $propertyConfiguration) { + if ($propertyConfiguration->getName() === $propertyName) { + continue 2; + } + } + + $this->logger->info('Class ' . $className . ' has property ' . $propertyName . ' that might be entity!'); + + $result = true; + } + + return $result; + } } diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php index 06e5607701..2fc3dbf36a 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php @@ -19,28 +19,22 @@ use Neos\Flow\Persistence\Aspect\PersistenceMagicInterface; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Utility\Arrays; -use Neos\Utility\Exception\PropertyNotAccessibleException; -use Neos\Utility\ObjectAccess; /** * Methods used to serialize objects used by proxy classes. */ trait ObjectSerializationTrait { - protected array $Flow_Object_PropertiesToSerialize = []; - - protected ?array $Flow_Persistence_RelatedEntities = null; - /** * Code to find and serialize entities on sleep * * @param array $transientProperties * @param array $propertyVarTags * @return array - * @throws PropertyNotAccessibleException */ private function Flow_serializeRelatedEntities(array $transientProperties, array $propertyVarTags): array { + $propertiesToSerialize = []; $reflectedClass = new \ReflectionClass(__CLASS__); $allReflectedProperties = $reflectedClass->getProperties(); foreach ($allReflectedProperties as $reflectionProperty) { @@ -49,8 +43,7 @@ private function Flow_serializeRelatedEntities(array $transientProperties, array 'Flow_Aop_Proxy_targetMethodsAndGroupedAdvices', 'Flow_Aop_Proxy_groupedAdviceChains', 'Flow_Aop_Proxy_methodIsInAdviceMode', - 'Flow_Persistence_RelatedEntities', - 'Flow_Object_PropertiesToSerialize', + 'Flow_Persistence_RelatedEntitiesContainer', 'Flow_Injected_Properties', ])) { continue; @@ -64,7 +57,10 @@ private function Flow_serializeRelatedEntities(array $transientProperties, array if (is_array($this->$propertyName) || ($this->$propertyName instanceof \ArrayObject || $this->$propertyName instanceof \SplObjectStorage || $this->$propertyName instanceof Collection)) { if (count($this->$propertyName) > 0) { foreach ($this->$propertyName as $key => $value) { - $this->Flow_searchForEntitiesAndStoreIdentifierArray((string)$key, $value, $propertyName); + $entityWasFound = $this->Flow_searchForEntitiesAndStoreIdentifierArray((string)$key, $value, $propertyName); + if ($entityWasFound) { + $propertiesToSerialize[] = 'Flow_Persistence_RelatedEntitiesContainer'; + } } } } @@ -82,29 +78,25 @@ private function Flow_serializeRelatedEntities(array $transientProperties, array } } if ($this->$propertyName instanceof DoctrineProxy || ($this->$propertyName instanceof PersistenceMagicInterface && !Bootstrap::$staticObjectManager->get(PersistenceManagerInterface::class)->isNewObject($this->$propertyName))) { - if (!isset($this->Flow_Persistence_RelatedEntities) || !is_array($this->Flow_Persistence_RelatedEntities)) { - $this->Flow_Persistence_RelatedEntities = []; - $this->Flow_Object_PropertiesToSerialize[] = 'Flow_Persistence_RelatedEntities'; + $entityWasFound = $this->Flow_searchForEntitiesAndStoreIdentifierArray('', $this->$propertyName, $propertyName); + if ($entityWasFound) { + $propertiesToSerialize[] = 'Flow_Persistence_RelatedEntitiesContainer'; } - $identifier = Bootstrap::$staticObjectManager->get(PersistenceManagerInterface::class)->getIdentifierByObject($this->$propertyName); - if (!$identifier && $this->$propertyName instanceof DoctrineProxy) { - $identifier = current(ObjectAccess::getProperty($this->$propertyName, '_identifier', true)); - } - $this->Flow_Persistence_RelatedEntities[$propertyName] = [ - 'propertyName' => $propertyName, - 'entityType' => $className, - 'identifier' => $identifier - ]; continue; } - if ($className !== false && (Bootstrap::$staticObjectManager->getScope($className) === Configuration::SCOPE_SINGLETON || $className === DependencyProxy::class)) { + if ($className !== false && + ( + Bootstrap::$staticObjectManager->getScope($className) === Configuration::SCOPE_SINGLETON + || Bootstrap::$staticObjectManager->getScope($className) === Configuration::SCOPE_SESSION + || $className === DependencyProxy::class + )) { continue; } } - $this->Flow_Object_PropertiesToSerialize[] = $propertyName; + $propertiesToSerialize[] = $propertyName; } - return $this->Flow_Object_PropertiesToSerialize; + return $propertiesToSerialize; } /** @@ -113,36 +105,36 @@ private function Flow_serializeRelatedEntities(array $transientProperties, array * @param string $path * @param mixed $propertyValue * @param string $originalPropertyName - * @return void + * @return bool if an entity was found */ - private function Flow_searchForEntitiesAndStoreIdentifierArray(string $path, mixed $propertyValue, string $originalPropertyName): void + private function Flow_searchForEntitiesAndStoreIdentifierArray(string $path, mixed $propertyValue, string $originalPropertyName): bool { + $entityWasFound = false; if (is_array($propertyValue) || ($propertyValue instanceof \ArrayObject || $propertyValue instanceof \SplObjectStorage)) { foreach ($propertyValue as $key => $value) { - $this->Flow_searchForEntitiesAndStoreIdentifierArray($path . '.' . $key, $value, $originalPropertyName); + $entityWasFound = $entityWasFound || $this->Flow_searchForEntitiesAndStoreIdentifierArray($path . '.' . $key, $value, $originalPropertyName); } } elseif ($propertyValue instanceof DoctrineProxy || ($propertyValue instanceof PersistenceMagicInterface && !Bootstrap::$staticObjectManager->get(PersistenceManagerInterface::class)->isNewObject($propertyValue))) { - if (!isset($this->Flow_Persistence_RelatedEntities) || !is_array($this->Flow_Persistence_RelatedEntities)) { - $this->Flow_Persistence_RelatedEntities = []; - $this->Flow_Object_PropertiesToSerialize[] = 'Flow_Persistence_RelatedEntities'; + if (!isset($this->Flow_Persistence_RelatedEntitiesContainer)) { + throw new \RuntimeException(sprintf('The class "%s" has an entity reference Flow could not detect, please add a Flow\\Proxy annotation with "forceSerializationCode" set to "true".', 1756936954)); } - if ($propertyValue instanceof DoctrineProxy) { - $className = get_parent_class($propertyValue); - } else { - $className = Bootstrap::$staticObjectManager->getObjectNameByClassName(get_class($propertyValue)); + $this->Flow_Persistence_RelatedEntitiesContainer->appendRelatedEntity($originalPropertyName, $path, $propertyValue); + /** + * The idea of setting to null here is to prevent serialization after we found an entity, BUT this logic + * is heavily flawed in today's PHP world. Type hinting might make null an invalid value. Also + * Arrays::setValueByPath() only works on "Array-like" not on objects, therefore + * we don't handle direct properties of $this (path empty string) at all here. + * They are skipped for serialization in Flow_serializeRelatedEntities so we don't need to unset. + * This still leaves the option of types going awry somewhere, but at the moment there + * isn't really a better solution at hand and the case should be super rare. + */ + if ($path !== '') { + $this->$originalPropertyName = Arrays::setValueByPath($this->$originalPropertyName, $path, null); } - $identifier = Bootstrap::$staticObjectManager->get(PersistenceManagerInterface::class)->getIdentifierByObject($propertyValue); - if (!$identifier && $propertyValue instanceof DoctrineProxy) { - $identifier = current(ObjectAccess::getProperty($propertyValue, '_identifier', true)); - } - $this->Flow_Persistence_RelatedEntities[$originalPropertyName . '.' . $path] = [ - 'propertyName' => $originalPropertyName, - 'entityType' => $className, - 'identifier' => $identifier, - 'entityPath' => $path - ]; - $this->$originalPropertyName = Arrays::setValueByPath($this->$originalPropertyName, $path, null); + $entityWasFound = true; } + + return $entityWasFound; } /** @@ -153,17 +145,18 @@ private function Flow_searchForEntitiesAndStoreIdentifierArray(string $path, mix */ private function Flow_setRelatedEntities(): void { - if (isset($this->Flow_Persistence_RelatedEntities)) { + if (isset($this->Flow_Persistence_RelatedEntitiesContainer)) { $persistenceManager = Bootstrap::$staticObjectManager->get(PersistenceManagerInterface::class); - foreach ($this->Flow_Persistence_RelatedEntities as $entityInformation) { - $entity = $persistenceManager->getObjectByIdentifier($entityInformation['identifier'], $entityInformation['entityType'], true); - if (isset($entityInformation['entityPath'])) { - $this->{$entityInformation['propertyName']} = Arrays::setValueByPath($this->{$entityInformation['propertyName']}, $entityInformation['entityPath'], $entity); + foreach ($this->Flow_Persistence_RelatedEntitiesContainer as $entityInformation) { + $entity = $persistenceManager->getObjectByIdentifier($entityInformation['i'], $entityInformation['c'], true); + if (isset($entityInformation['p'])) { + $this->{$entityInformation['n']} = Arrays::setValueByPath($this->{$entityInformation['n']}, $entityInformation['p'], $entity); } else { - $this->{$entityInformation['propertyName']} = $entity; + $this->{$entityInformation['n']} = $entity; } } - unset($this->Flow_Persistence_RelatedEntities); + + isset($this->Flow_Persistence_RelatedEntitiesContainer) && $this->Flow_Persistence_RelatedEntitiesContainer->reset(); } } } diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php index d414df4d4d..0fd1d0efaf 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php @@ -115,11 +115,11 @@ public function injectReflectionService(ReflectionService $reflectionService): v * @throws \ReflectionException * @throws CannotBuildObjectException */ - public function getConstructor(): ProxyConstructorGenerator + public function getConstructor(bool $withOriginalArgumentSignature = false): ProxyConstructorGenerator { if (!isset($this->constructor)) { if (method_exists($this->fullOriginalClassName, '__construct')) { - $this->constructor = ProxyConstructorGenerator::fromReflection(new MethodReflection($this->fullOriginalClassName, '__construct')); + $this->constructor = ProxyConstructorGenerator::fromReflection(new MethodReflection($this->fullOriginalClassName, '__construct'), $withOriginalArgumentSignature); } else { $this->constructor = new ProxyConstructorGenerator(); $this->constructor->setFullOriginalClassName($this->fullOriginalClassName); @@ -165,12 +165,12 @@ public function addConstant(string $name, string $valueCode): void * Adds a class property to this proxy class * * @param string $name Name of the property - * @param string $initialValueCode PHP code of the initial value assignment + * @param string|null $initialValueCode PHP code of the initial value assignment * @param string $visibility * @param string $docComment * @return void */ - public function addProperty(string $name, string $initialValueCode, string $visibility = 'private', string $docComment = ''): void + public function addProperty(string $name, string|null $initialValueCode, string $visibility = 'private', string $docComment = ''): void { // TODO: Add support for PHP attributes? $this->properties[$name] = [ @@ -194,6 +194,15 @@ public function addInterfaces(array $interfaceNames): void $this->interfaces = array_merge($this->interfaces, $interfaceNames); } + /** + * Inspect currently added interfaces for this proxy class + * @return array|string[] + */ + public function getInterfaces(): array + { + return $this->interfaces; + } + /** * Adds one or more traits to the class definition. * @@ -308,7 +317,7 @@ protected function renderPropertiesCode(): string if (!empty($attributes['docComment'])) { $code .= ' ' . $attributes['docComment'] . "\n"; } - $code .= ' ' . $attributes['visibility'] . ' $' . $name . ' = ' . $attributes['initialValueCode'] . ";\n"; + $code .= ' ' . $attributes['visibility'] . ' $' . $name . ($attributes['initialValueCode'] ? (' = ' . $attributes['initialValueCode']) : '') . ";\n"; } return $code; } diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php index 4697909129..c6405db34b 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php @@ -12,6 +12,8 @@ */ use Laminas\Code\Generator\DocBlockGenerator; +use Laminas\Code\Generator\ParameterGenerator; +use Laminas\Code\Generator\PromotedParameterGenerator; use Neos\Flow\ObjectManagement\DependencyInjection\ProxyClassBuilder; final class ProxyConstructorGenerator extends ProxyMethodGenerator @@ -30,7 +32,7 @@ public function __construct($name = null, array $parameters = [], $flags = self: parent::__construct('__construct', $parameters, $flags, $body, $docBlock); } - public static function fromReflection(\Laminas\Code\Reflection\MethodReflection $reflectionMethod): static + public static function fromReflection(\Laminas\Code\Reflection\MethodReflection $reflectionMethod, bool $withOriginalArgumentSignature = false): static { $method = new static('__construct'); $declaringClass = $reflectionMethod->getDeclaringClass(); @@ -56,6 +58,17 @@ public static function fromReflection(\Laminas\Code\Reflection\MethodReflection $docBlock->setWordWrap(false); $docBlock->setSourceDirty(false); $method->setDocBlock($docBlock); + + if ($withOriginalArgumentSignature) { + foreach ($reflectionMethod->getParameters() as $reflectionParameter) { + $method->setParameter( + $reflectionParameter->isPromoted() + ? PromotedParameterGenerator::fromReflection($reflectionParameter) + : ParameterGenerator::fromReflection($reflectionParameter) + ); + } + } + return $method; } diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/RelatedEntitiesContainer.php b/Neos.Flow/Classes/ObjectManagement/Proxy/RelatedEntitiesContainer.php new file mode 100644 index 0000000000..934029b23c --- /dev/null +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/RelatedEntitiesContainer.php @@ -0,0 +1,52 @@ +e as $entityInformation) { + yield $entityInformation; + } + } + + public function reset(): void + { + $this->e = []; + } + + public function appendRelatedEntity(string $originalPropertyName, string $path, object $propertyValue): void + { + if ($propertyValue instanceof DoctrineProxy) { + $className = get_parent_class($propertyValue); + } else { + $className = Bootstrap::$staticObjectManager->getObjectNameByClassName(get_class($propertyValue)); + } + $identifier = Bootstrap::$staticObjectManager->get(PersistenceManagerInterface::class)->getIdentifierByObject($propertyValue); + if (!$identifier && $propertyValue instanceof DoctrineProxy) { + $identifier = current(ObjectAccess::getProperty($propertyValue, '_identifier', true)); + } + + $this->e[$originalPropertyName . '.' . $path] = [ + 'n' => $originalPropertyName, + 'c' => $className, + 'i' => $identifier, + 'p' => $path + ]; + } +} diff --git a/Neos.Flow/Configuration/Objects.yaml b/Neos.Flow/Configuration/Objects.yaml index daae5adee2..13b2bfa8b5 100644 --- a/Neos.Flow/Configuration/Objects.yaml +++ b/Neos.Flow/Configuration/Objects.yaml @@ -467,6 +467,7 @@ Neos\Flow\Session\SessionInterface: factoryMethodName: getCurrentSession Neos\Flow\Session\Data\SessionKeyValueStore: + scope: singleton properties: cache: object: @@ -477,6 +478,7 @@ Neos\Flow\Session\Data\SessionKeyValueStore: value: Flow_Session_Storage Neos\Flow\Session\Data\SessionMetaDataStore: + scope: singleton properties: cache: object: diff --git a/Neos.Flow/Configuration/Settings.Object.yaml b/Neos.Flow/Configuration/Settings.Object.yaml index 4e6efb37e2..7c821b84c9 100644 --- a/Neos.Flow/Configuration/Settings.Object.yaml +++ b/Neos.Flow/Configuration/Settings.Object.yaml @@ -51,3 +51,15 @@ Neos: # - '^Neos\\SomePackage\\ValueObjects\\SomeSpecificValueObject$' # excludeClassesFromConstructorAutowiring: [] + + # This is a forward compatibility setting when used with "false" preventing constructor arguments to be autowired + # if they are not declared singleton or session. + # + # BEWARE of dragons, Flow code should be fine, but Neos might still contain such constructs. + # Manual full cache flush is necessary after changing this setting. + # + # Relying on the legacy behaviour is deprecated and will be logged to the system log as debug entry. + # + # In the future autowiring of prototypes should no longer be an option as it makes no sense. + # Use a factory class if you need this otherwise. + constructorAutowiringWithPrototypes: true diff --git a/Neos.Flow/Resources/Private/Schema/Settings/Neos.Flow.object.schema.yaml b/Neos.Flow/Resources/Private/Schema/Settings/Neos.Flow.object.schema.yaml index 8e325db365..75b703b1ef 100644 --- a/Neos.Flow/Resources/Private/Schema/Settings/Neos.Flow.object.schema.yaml +++ b/Neos.Flow/Resources/Private/Schema/Settings/Neos.Flow.object.schema.yaml @@ -14,3 +14,5 @@ properties: excludeClassesFromConstructorAutowiring: type: array items: { type: string, required: true } + constructorAutowiringWithPrototypes: + type: boolean diff --git a/Neos.Flow/Tests/Functional/ObjectManagement/DependencyInjectionTest.php b/Neos.Flow/Tests/Functional/ObjectManagement/DependencyInjectionTest.php index 712c5accae..9124e2aa5b 100644 --- a/Neos.Flow/Tests/Functional/ObjectManagement/DependencyInjectionTest.php +++ b/Neos.Flow/Tests/Functional/ObjectManagement/DependencyInjectionTest.php @@ -16,9 +16,11 @@ use Neos\Flow\ObjectManagement\Proxy\ProxyInterface; use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\FinalClassWithDependencies; use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\Flow175\ClassWithTransitivePrototypeDependency; +use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\InterfaceAImplementation; use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PrototypeClassA; use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PrototypeClassH; use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\PrototypeClassL; +use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\SingletonClassH; use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\SingletonClassA; use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\ValueObjectClassA; use Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\ValueObjectClassB; @@ -363,4 +365,24 @@ public function constructorSettingsInjectionViaInjectAnnotation(): void $object = new PrototypeClassL('override'); self::assertSame('override', $object->value); } + + /** + * @test + */ + public function constructorObjectInjectionInSingletonWithDefaultNullValue(): void + { + // FIXME Singletons with a default value of null as constructor are deprecated. The property declaration is simply redundant as the dependency is always null and never set by the object management - even if there is a union with another type. + $object = $this->objectManager->get(SingletonClassH::class); + self::assertInstanceOf(ProxyInterface::class, $object); + // no dependency as default is always null -> thus why its useless + self::assertSame(null, $object->interfaceA); + + // explicitly set to null - DEPRECATED don't instantiate singletons by hand + $object = new SingletonClassH(null); + self::assertSame(null, $object->interfaceA); + + // explicitly provide dependency - DEPRECATED don't instantiate singletons by hand + $object = new SingletonClassH($this->objectManager->get(InterfaceAImplementation::class)); + self::assertInstanceOf(InterfaceAImplementation::class, $object->interfaceA); + } } diff --git a/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithEntityProperty.php b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithEntityProperty.php new file mode 100644 index 0000000000..e5921f6e07 --- /dev/null +++ b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithEntityProperty.php @@ -0,0 +1,40 @@ +entity = $entity; + $this->someValue = $someValue; + } +} diff --git a/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SimpleEntity.php b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SimpleEntity.php new file mode 100644 index 0000000000..d71d813733 --- /dev/null +++ b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SimpleEntity.php @@ -0,0 +1,37 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SingletonClassH.php b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SingletonClassH.php new file mode 100644 index 0000000000..a4bd89ffd5 --- /dev/null +++ b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SingletonClassH.php @@ -0,0 +1,30 @@ +someValue); + } +}