Skip to content

Conversation

@kitsunet
Copy link
Member

@kitsunet kitsunet commented Sep 9, 2025

Props to @lorenzulrich for this correct fix, which always gets the
parent in relation to the method being called and not the parent of
the instance at hand.

Fixes: #3406

This is an overhaul of how object serialization
is prepared in proxies.

Proxies can skip object serialization code if there is nothing to
serialize, that is, if there are no entity properties, no injected, or
transient properties. We were too eager prior to this patch with
not using the serialization code, the checks are now way more detailed.
Additionally the "Proxy" Annotation now allows to force serialization
code for a class if the checks still fail to detect correctly, this
should be rarely needed.
This fix however broke some code in Neos that should have gotten the
serialization code previously but didn't. Since the class in question
is readonly, injecting a mutable property via trait resulted in
PHP errors.
Therefore we now use a mutable object to hold related entities for
serialization purposes which is declared readonly in the proxy to
avoid errors with readonly classes should they need serialization
code. Other mutable properties were removed as they are not strictly
needed. We should do the same refactoring for AOP as well.
Proxies can use the original constructor argument signature if no
constructor injection is used.
Finally prototype autowiring is now a choice via setting, currently
default enabled to not change behavior, in the future we should plan
a breaking change to disable it and then remove the option altogether.

Fixes: #3493
Related: #3212
Related: #3076

Props to @lorenzulrich for this correct fix, which always gets the
parent in relation to the method being called and not the parent of
the instance at hand.

Fixes: neos#3406
This allows classes without constructor injection to have a proper
constructor signature.
This is an overhaul of how object serialization
is prepared in proxies.

Proxies can skip object serialization code if there is nothing to
serialize, that is, if there are no entity properties, no injected, or
transient properties. We were too eager prior to this patch with
not using the serialization code, the checks are now way more detailed.
Additionally the "Proxy" Annotation now allows to force serialization
code for a class if the checks still fail to detect correctly, this
should be rarely needed.
This fix however broke some code in Neos that should have gotten the
serialization code previously but didn't. Since the class in question
is readonly, injecting a mutable property via trait resulted in
PHP errors.
Therefore we now use a mutable object to hold related entities for
serialization purposes which is declared readonly in the proxy to
avoid errors with readonly classes should they need serialization
code. Other mutable properties were removed as they are not strictly
needed. We should do the same refactoring for AOP as well.
Proxies can use the original constructor argument signature if no
constructor injection is used.
Finally prototype autowiring is now a choice via setting, currently
default enabled to not change behavior, in the future we should plan
a breaking change to disable it and then remove the option altogether.

Fixes: neos#3493
Related: neos#3212
Related: neos#3076
kitsunet added a commit to kitsunet/neos-development-collection that referenced this pull request Sep 9, 2025
This removes occurances of constructor injection with prototypes, the
modified classes are all injected via constructor and should therefore
be singletons.

Related: neos/flow-development-collection#3494
@kitsunet
Copy link
Member Author

kitsunet commented Sep 9, 2025

Interesting test failures... because that did work fine... Investigating...

@lorenzulrich
Copy link
Contributor

Thanks @kitsunet. This is "above my pay grade", so thanks for taking care and finding the reason why my code works ;-).

@kitsunet
Copy link
Member Author

Ok funny, now this PR shows one of the errors I wanted to fix in the functional tests. Why? Well because I broke something (that is if an object already has a sleep method we do not proxy anything) but intererstingly this scenario is AFAIK not happening in Flow nor Neos, nor have I seen it in any of the projects I checked. Anyways, will fix, seems like a good idea.

neos-bot pushed a commit to neos/neos that referenced this pull request Oct 18, 2025
This removes occurances of constructor injection with prototypes, the
modified classes are all injected via constructor and should therefore
be singletons.

Related: neos/flow-development-collection#3494
@robertlemke
Copy link
Member

Some documentation for the new flag - I don't know where to put it, maybe into this PR's description? Or the manual?

When Flow generates proxy classes, it automatically detects if your class contains entity properties
(properties typed with classes annotated as @Flow\Entity) or other framework-managed objects
that require special handling during serialization. In such cases, Flow automatically generates
__sleep() and __wakeup() methods that:

  • Convert entity references to metadata (class name and persistence identifier) before serialization
  • Remove injected and transient properties
  • Restore entity references after deserialization

This detection is automatic and works in most cases. However, in rare edge cases where the automatic
detection fails (e.g., with complex generic types, deeply nested entity structures, or unusual type
declarations), you can force the generation of serialization code using the @Flow\Proxy annotation::

	use Neos\Flow\Annotations as Flow;

	/**
	 * @Flow\Proxy(forceSerializationCode=true)
	 */
	class ComplexObjectWithEntities {

		/**
		 * @var ComplexGenericType<SomeEntity>
		 */
		protected $complexProperty;

	}

You should rarely need to use forceSerializationCode. If you find yourself needing it
for standard entity properties or injected dependencies, this indicates a bug in Flow's
automatic detection that should be reported.

Copy link
Member

@robertlemke robertlemke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I tested this locally based on the functional tests and verified the difference of the generated proxy classes. Fine to merge, I guess?

@robertlemke robertlemke force-pushed the bugfix/proxy-fix-overhaul branch from 53be99c to 14c41f0 Compare October 20, 2025 15:17
* see references to it in serialized object strings.
* @internal
*/
#[Flow\Proxy(false)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? Or an optimization / safeguard?

private function Flow_searchForEntitiesAndStoreIdentifierArray(string $path, mixed $propertyValue, string $originalPropertyName): void
private function Flow_searchForEntitiesAndStoreIdentifierArray(string $path, mixed $propertyValue, string $originalPropertyName): bool
{
$foundEntity = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$foundEntity = false;
$entityWasFound = false;

like in the code above – and $foundEntity sounds as if it contains the entity that was found.

if (is_array($propertyValue) || ($propertyValue instanceof \ArrayObject || $propertyValue instanceof \SplObjectStorage)) {
foreach ($propertyValue as $key => $value) {
$this->Flow_searchForEntitiesAndStoreIdentifierArray($path . '.' . $key, $value, $originalPropertyName);
$foundEntity = $foundEntity || $this->Flow_searchForEntitiesAndStoreIdentifierArray($path . '.' . $key, $value, $originalPropertyName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$foundEntity = $foundEntity || $this->Flow_searchForEntitiesAndStoreIdentifierArray($path . '.' . $key, $value, $originalPropertyName);
$entityWasFound = $entityWasFound || $this->Flow_searchForEntitiesAndStoreIdentifierArray($path . '.' . $key, $value, $originalPropertyName);

'entityPath' => $path
];
$this->$originalPropertyName = Arrays::setValueByPath($this->$originalPropertyName, $path, null);
$foundEntity = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$foundEntity = true;
$entityWasFound = true;

$foundEntity = true;
}

return $foundEntity;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return $foundEntity;
return $entityWasFound;

protected function couldHaveEntityRelations(Configuration $objectConfiguration): bool
{
$result = false;
/** @var class-string $className */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is class-string on a plain @var something useful?

#
# 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.
prototypeAutowiring: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is not as clear to me, as it should be. Does this prevent the autowiring of constructor arguments to prototypes or does it prevent the use of prototypes as constructor arguments?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Incorrectly missing __sleep for proxy classes with entity properties and/or advices in some cases

4 participants