Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions Neos.Flow/Classes/Annotations/Proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Entity>)
* - 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;
}
}
1 change: 1 addition & 0 deletions Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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));

Expand Down Expand Up @@ -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);
Expand All @@ -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 '';
}

Expand All @@ -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 '';
}

Expand All @@ -246,16 +256,14 @@ 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
);
}

protected function buildSetRelatedEntitiesCode(): string
{
return "\n " . '$this->Flow_setRelatedEntities();' . "\n";
return "\n" . '$this->Flow_setRelatedEntities();' . "\n";
}

/**
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}
Loading
Loading