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?

@mhsdesign mhsdesign changed the base branch from 9.0 to 9.1 December 12, 2025 16:21
@github-actions github-actions bot added 9.1 and removed 9.0 labels Dec 12, 2025
@mhsdesign
Copy link
Member

mhsdesign commented Dec 12, 2025

Okay i found a bug last minute

Regression:

During testing i had Neos Debug installed and the proxies generated failed at php compile time:

Fatal error: Type of Flowpack\Neos\Debug\DataCollector\MessagesCollector::$dataFormatter must be ?Flowpack\Neos\Debug\DataFormatter\DataFormatterInterface (as in class Flowpack\Neos\Debug\DataCollector\AbstractDataCollector) in Data/Temporary/Development/Cache/Code/Flow_Object_Classes/Flowpack_Neos_Debug_DataCollector_MessagesCollector.php on line 40

The reason for this is the constructor in the abstract class: https://github.com/Flowpack/neos-debug/blob/9c82cb345cb1b786b88f07dee73824882479a21d/Classes/DataCollector/AbstractDataCollector.php#L12

public function __construct(
    protected ?DataFormatterInterface $dataFormatter = null,
) {
}

Now the object extending the AbstractDataCollector got in 9.x a proxy like

public function __construct()
{
    $arguments = func_get_args();
    if (get_class($this) === 'Flowpack\Neos\Debug\DataCollector\MessagesCollector') \Neos\Flow\Core\Bootstrap::$staticObjectManager->setInstance('Flowpack\Neos\Debug\DataCollector\MessagesCollector', $this);
    parent::__construct(...$arguments);
}

But with this change (prototypeAutowiring untouched!) the flow proxy gets generated like this by default:

public function __construct(protected \Flowpack\Neos\Debug\DataFormatter\DataFormatterInterface $dataFormatter)
{
    $arguments = func_get_args();
    if (get_class($this) === 'Flowpack\Neos\Debug\DataCollector\MessagesCollector') \Neos\Flow\Core\Bootstrap::$staticObjectManager->setInstance('Flowpack\Neos\Debug\DataCollector\MessagesCollector', $this);
    parent::__construct(...$arguments);
}

Where the default value and the nullability is missing.

EDIT: Found a possibly hack fix c7e76f9 but it seems we might need laminas/laminas-code#182 in reality?

…erty of singleton and hotfix test

Hotfix, we can only create the constructor method signature as originally if there are no default values involved.
As laminas does not have support for promoted properties with default values yet:
laminas/laminas-code#182

see also neos#3494 (comment)

The test fails otherwise with

```
PHP Fatal error:  Type of Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\SingletonClassH::$interfaceA must be ?Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\InterfaceA (as in class Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\SingletonClassH_Original)
```

As we write

```
public function __construct(public \Neos\Flow\Tests\Functional\ObjectManagement\Fixtures\InterfaceA $interfaceA)
```

to the constructor.

Now we generate a simple constructor again without properties `public function __construct()`
@mhsdesign
Copy link
Member

and also another oddity during the depreciation logging - there are much more entries than i would have anticipated in which the user of Neos and Flow cant do anything. Even Flow itself seems to raise the deprecation for its own code. I fear that we cannot distinguish between needing the auto-wiring and that Flow just gave that gratis until now. We should probably specify autowiring off for the respective constructors.

The list shows the prototype first entry where it is used in the constructor ... nested entries.

  • Neos\ContentRepositoryRegistry\Migration\Factory\MigrationFactory (prototype):

    • Neos\ContentRepositoryRegistry\Command\NodeMigrationCommandController
  • Neos\ContentRepositoryRegistry\Service\NodeMigrationGeneratorService (prototype):

    • Neos\ContentRepositoryRegistry\Command\NodeMigrationCommandController
  • Neos\ContentRepository\LegacyNodeMigration\RootNodeTypeMapping (prototype):

    • Neos\ContentRepository\LegacyNodeMigration\LegacyExportService
    • Neos\ContentRepository\LegacyNodeMigration\LegacyExportServiceFactory
    • Neos\ContentRepository\LegacyNodeMigration\Processors\EventExportProcessor
  • Neos\Flow\Aop\JoinPoint (prototype):

    • Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilegeSubject
  • Neos\Flow\I18n\Locale (prototype):

    • Neos\Flow\I18n\Xliff\Model\FileAdapter
  • Neos\Flow\Mvc\ActionRequest (prototype):

    • Neos\Flow\Mvc\Controller\ControllerContext
    • Neos\Form\Core\Runtime\FormRuntime
    • Neos\Fusion\Core\LegacyFusionControllerContext
  • Neos\Flow\Mvc\ActionResponse (prototype):

    • Neos\Flow\Mvc\Controller\ControllerContext
    • Neos\Form\Core\Runtime\FormRuntime
    • Neos\Fusion\Core\LegacyFusionControllerContext
  • Neos\Flow\Mvc\Controller\Arguments (prototype):

    • Neos\Flow\Mvc\Controller\ControllerContext
  • Neos\Flow\Mvc\Routing\UriBuilder (prototype):

    • Neos\Flow\Mvc\Controller\ControllerContext
  • Neos\Flow\Persistence\Doctrine\Query (prototype):

    • Neos\Flow\Persistence\Doctrine\QueryResult
    • Neos\Flow\Persistence\EmptyQueryResult
    • Neos\Media\Domain\Model\AssetSource\Neos\NeosAssetProxyQuery
  • Neos\Flow\ResourceManagement\PersistentResource (prototype):

    • Neos\Media\Domain\Model\Asset
    • Neos\Media\Domain\Model\Audio
    • Neos\Media\Domain\Model\Document
    • Neos\Media\Domain\Model\Image
    • Neos\Media\Domain\Model\Video
  • Neos\Flow\Security\Authorization\Privilege\PrivilegeTarget (prototype):

    • Neos\Media\Security\Authorization\Privilege\ReadAssetCollectionPrivilege
    • Neos\Media\Security\Authorization\Privilege\ReadAssetPrivilege
    • Neos\Media\Security\Authorization\Privilege\ReadTagPrivilege
    • Neos\Neos\Security\Authorization\Privilege\EditNodePrivilege
    • Neos\Neos\Security\Authorization\Privilege\ModulePrivilege
    • Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege
  • Neos\Form\Core\Model\FormDefinition (prototype):

    • Neos\Form\Core\Runtime\FormRuntime
  • Neos\Form\Core\Runtime\FormRuntime (prototype):

    • Neos\Form\Core\Model\FinisherContext
  • Neos\Fusion\Core\Cache\FusionContextSerializer (prototype):

    • Neos\Neos\Fusion\Cache\NeosFusionContextSerializer
  • Neos\Fusion\Core\FusionConfiguration (prototype):

    • Neos\Fusion\Core\Runtime
  • Neos\Fusion\Core\FusionGlobals (prototype):

    • Neos\Fusion\Core\Runtime
  • Neos\Fusion\Core\ObjectTreeParser\Lexer (prototype):

    • Neos\Fusion\Core\ObjectTreeParser\ObjectTreeParser
  • Neos\Fusion\Core\ObjectTreeParser\MergedArrayTree (prototype):

    • Neos\Fusion\Core\ObjectTreeParser\MergedArrayTreeVisitor
  • Neos\Fusion\Core\Runtime (prototype):

    • Neos\Fusion\Core\Cache\RuntimeContentCache
    • all objects of type AbstractFusionObject because of the shared constructor
  • Neos\Fusion\FusionObjects\AbstractFusionObject (prototype):

    • Neos\Fusion\FusionObjects\Helpers\FluidView
  • Neos\Fusion\FusionObjects\TemplateImplementation (prototype):

    • Neos\Fusion\FusionObjects\Helpers\FusionPathProxy
  • Neos\Http\Factories\UriFactory (prototype):

    • Neos\Http\Factories\PsrHttpFactory
    • Neos\Http\Factories\ServerRequestFactory
  • Neos\Media\Domain\Model\AssetSource\Neos\NeosAssetSource (prototype):

    • Neos\Media\Domain\Model\AssetSource\Neos\NeosAssetProxy
    • Neos\Media\Domain\Model\AssetSource\Neos\NeosAssetProxyQuery
    • Neos\Media\Domain\Model\AssetSource\Neos\NeosAssetProxyRepository
  • Neos\Media\Domain\Model\Image (prototype):

    • Neos\Media\Browser\Domain\ImageMapper
    • Neos\Media\Domain\Model\ImageVariant
  • Neos\Media\Domain\Model\ThumbnailConfiguration (prototype):

    • Neos\Media\Domain\Model\Thumbnail
  • Neos\Media\Domain\ValueObject\Configuration\Label (prototype):

    • Neos\Media\Domain\ValueObject\Configuration\VariantPreset
  • Neos\Neos\AssetUsage\AssetUsageIndexingProcessor (prototype):

    • Neos\Neos\AssetUsage\Command\AssetUsageCommandController
  • Neos\Neos\Domain\SubtreeTagging\SoftRemoval\SoftRemovalGarbageCollector (prototype):

    • Neos\Neos\Domain\Service\WorkspacePublishingService
    • Neos\Neos\Domain\Service\WorkspaceService
  • Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolver\Segments (prototype):

    • Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolver
  • Neos\Neos\FrontendRouting\Projection\DocumentUriPathFinder (prototype):

    • Neos\Neos\FrontendRouting\CatchUpHook\RouterCacheHook
    • Neos\RedirectHandler\NeosAdapter\CatchUpHook\DocumentUriPathProjectionHook
  • Neos\Neos\Setup\Infrastructure\ImageHandler\ImageHandlerService (prototype):

    • Neos\Neos\Setup\Infrastructure\Healthcheck\ImageHandlerHealthcheck
  • Neos\Setup\Domain\HealthcheckEnvironment (prototype):

    • Neos\Setup\Infrastructure\HealthChecker
  • Neos\TimeableNodeVisibility\Domain\ChangedVisibilityType (prototype):

    • Neos\TimeableNodeVisibility\Domain\ChangedVisibility
  • TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler (prototype):

    • TYPO3Fluid\Fluid\Core\Compiler\NodeConverter
  • TYPO3Fluid\Fluid\Core\Parser\ParsingState (prototype):

    • TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode

…alue are useless

A better? Fix might be to never keep the original signature for singletons as they are crated via the object framework from the inside anyway... though maybe not in the future.

```
$constructor = $proxyClass->getConstructor(withOriginalArgumentSignature: $objectConfiguration->getScope() === Configuration::SCOPE_PROTOTYPE && $injectionCodeWasIntroduced === false);
```
@kitsunet
Copy link
Member Author

Mmm, technically I think the constructor change doesn't have to live in here, I guess I can extract it and try to put it in the runtime constructor injection change, probably more thematic there anyways. Then this becomes a bit simpler.

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

Projects

Status: In progress

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

5 participants