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 a175e0cf8d..a8a6fa7d2d 100644 --- a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php @@ -409,14 +409,14 @@ public function buildProxyClass(string $targetClassName, array $aspectContainers $proxyClass->addProperty($propertyName, var_export($propertyIntroduction->getInitialValue(), true), $propertyIntroduction->getPropertyVisibility(), $propertyIntroduction->getPropertyDocComment()); } - $proxyClass->getMethod('Flow_Aop_Proxy_buildMethodsAndAdvicesArray')->addPreParentCallCode(" if (method_exists(get_parent_class(\$this), 'Flow_Aop_Proxy_buildMethodsAndAdvicesArray') && is_callable([parent::class, 'Flow_Aop_Proxy_buildMethodsAndAdvicesArray'])) parent::Flow_Aop_Proxy_buildMethodsAndAdvicesArray();\n"); + $proxyClass->getMethod('Flow_Aop_Proxy_buildMethodsAndAdvicesArray')->addPreParentCallCode(" if (method_exists(parent::class, 'Flow_Aop_Proxy_buildMethodsAndAdvicesArray') && is_callable([parent::class, 'Flow_Aop_Proxy_buildMethodsAndAdvicesArray'])) parent::Flow_Aop_Proxy_buildMethodsAndAdvicesArray();\n"); $proxyClass->getMethod('Flow_Aop_Proxy_buildMethodsAndAdvicesArray')->addPreParentCallCode($this->buildMethodsAndAdvicesArrayCode($interceptedMethods)); $proxyClass->getMethod('Flow_Aop_Proxy_buildMethodsAndAdvicesArray')->setVisibility(ProxyMethodGenerator::VISIBILITY_PROTECTED); $callBuildMethodsAndAdvicesArrayCode = "\n \$this->Flow_Aop_Proxy_buildMethodsAndAdvicesArray();\n"; $proxyClass->getConstructor()->addPreParentCallCode($callBuildMethodsAndAdvicesArrayCode); $proxyClass->getMethod('__wakeup')->addPreParentCallCode($callBuildMethodsAndAdvicesArrayCode); - $proxyClass->getMethod('__clone')->addPreParentCallCode($callBuildMethodsAndAdvicesArrayCode); + $proxyClass->getMethod('__clone')->setReturnType('void')->addPreParentCallCode($callBuildMethodsAndAdvicesArrayCode); if (!$this->reflectionService->hasMethod($targetClassName, '__wakeup')) { $proxyClass->getMethod('__wakeup')->addPostParentCallCode(<<addTraits(['\\' . AdvicesTrait::class]); + $proxyClass->addInterfaces(['\\' . Aop\ProxyInterface::class]); $this->buildMethodsInterceptorCode($targetClassName, $interceptedMethods); diff --git a/Neos.Flow/Classes/Cli/CommandManager.php b/Neos.Flow/Classes/Cli/CommandManager.php index bf28383652..330b1d158c 100644 --- a/Neos.Flow/Classes/Cli/CommandManager.php +++ b/Neos.Flow/Classes/Cli/CommandManager.php @@ -27,42 +27,19 @@ class CommandManager { /** - * @var array + * @var array|null */ - protected $availableCommands = null; + protected array|null $availableCommands = null; /** - * @var array + * @var array|null */ - protected $shortCommandIdentifiers = null; + protected array|null $shortCommandIdentifiers = null; - /** - * @var Bootstrap - */ - protected $bootstrap; - - /** - * @var ObjectManagerInterface - */ - protected $objectManager; - - /** - * @param ObjectManagerInterface $objectManager - * @return void - */ - public function injectObjectManager(ObjectManagerInterface $objectManager) - { - $this->objectManager = $objectManager; - } - - /** - * @param Bootstrap $bootstrap - * @return void - */ - public function injectBootstrap(Bootstrap $bootstrap) - { - $this->bootstrap = $bootstrap; - } + public function __construct( + private readonly Bootstrap $bootstrap, + private readonly ObjectManagerInterface $objectManager + ) {} /** * Returns an array of all commands diff --git a/Neos.Flow/Classes/Cli/Dispatcher.php b/Neos.Flow/Classes/Cli/Dispatcher.php index 9b123c6b31..04a278791f 100644 --- a/Neos.Flow/Classes/Cli/Dispatcher.php +++ b/Neos.Flow/Classes/Cli/Dispatcher.php @@ -14,31 +14,10 @@ */ class Dispatcher { - /** - * @var \Neos\Flow\SignalSlot\Dispatcher - */ - protected $signalDispatcher; - - /** - * @var ObjectManagerInterface - */ - protected $objectManager; - - /** - * @param \Neos\Flow\SignalSlot\Dispatcher $signalDispatcher - */ - public function injectSignalDispatcher(\Neos\Flow\SignalSlot\Dispatcher $signalDispatcher) - { - $this->signalDispatcher = $signalDispatcher; - } - - /** - * @param ObjectManagerInterface $objectManager - */ - public function injectObjectManager(ObjectManagerInterface $objectManager) - { - $this->objectManager = $objectManager; - } + public function __construct( + protected \Neos\Flow\SignalSlot\Dispatcher $signalDispatcher, + protected ObjectManagerInterface $objectManager + ) {} /** * Try processing the request until it is successfully marked "dispatched" diff --git a/Neos.Flow/Classes/Cli/RequestBuilder.php b/Neos.Flow/Classes/Cli/RequestBuilder.php index df1ee24411..cc206bfc29 100644 --- a/Neos.Flow/Classes/Cli/RequestBuilder.php +++ b/Neos.Flow/Classes/Cli/RequestBuilder.php @@ -45,61 +45,12 @@ class RequestBuilder ) /x'; - /** - * @var Environment - */ - protected $environment; - - /** - * @var ObjectManagerInterface - */ - protected $objectManager; - - /** - * @var PackageManager - */ - protected $packageManager; - - /** - * @var CommandManager - */ - protected $commandManager; - - /** - * @param Environment $environment - * @return void - */ - public function injectEnvironment(Environment $environment) - { - $this->environment = $environment; - } - - /** - * @param ObjectManagerInterface $objectManager - * @return void - */ - public function injectObjectManager(ObjectManagerInterface $objectManager) - { - $this->objectManager = $objectManager; - } - - /** - * @param PackageManager $packageManager - * @return void - */ - public function injectPackageManager(PackageManager $packageManager) - { - $this->packageManager = $packageManager; - } - - /** - * @param CommandManager $commandManager - * @return void - */ - public function injectCommandManager(CommandManager $commandManager) - { - $this->commandManager = $commandManager; - } + public function __construct( + protected Environment $environment, + protected ObjectManagerInterface $objectManager, + protected PackageManager $packageManager, + protected CommandManager $commandManager + ) {} /** * Builds a CLI request object from a command line. diff --git a/Neos.Flow/Classes/Command/CoreCommandController.php b/Neos.Flow/Classes/Command/CoreCommandController.php index 4e1607e0e8..2776735063 100644 --- a/Neos.Flow/Classes/Command/CoreCommandController.php +++ b/Neos.Flow/Classes/Command/CoreCommandController.php @@ -189,11 +189,8 @@ public function compileCommand(bool $force = false) $classesCache = $this->cacheManager->getCache('Flow_Object_Classes'); $logger = $this->objectManager->get(PsrLoggerFactoryInterface::class)->get('systemLogger'); - $this->proxyClassCompiler->injectClassesCache($classesCache); - $this->aopProxyClassBuilder->injectObjectConfigurationCache($objectConfigurationCache); $this->aopProxyClassBuilder->injectLogger($logger); - $this->dependencyInjectionProxyClassBuilder->injectLogger($logger); $this->aopProxyClassBuilder->build(); $this->dependencyInjectionProxyClassBuilder->build(); diff --git a/Neos.Flow/Classes/Core/Booting/Scripts.php b/Neos.Flow/Classes/Core/Booting/Scripts.php index 74edfed2e0..978badee9b 100644 --- a/Neos.Flow/Classes/Core/Booting/Scripts.php +++ b/Neos.Flow/Classes/Core/Booting/Scripts.php @@ -483,7 +483,6 @@ public static function initializeObjectManagerCompileTimeFinalize(Bootstrap $boo $logger = $bootstrap->getEarlyInstance(PsrLoggerFactoryInterface::class)->get('systemLogger'); $packageManager = $bootstrap->getEarlyInstance(PackageManager::class); - $objectManager->injectAllSettings($configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS)); $objectManager->injectReflectionService($reflectionService); $objectManager->injectConfigurationManager($configurationManager); $objectManager->injectConfigurationCache($cacheManager->getCache('Flow_Object_Configuration')); @@ -510,7 +509,6 @@ public static function initializeObjectManager(Bootstrap $bootstrap) $objectManager = new ObjectManager($bootstrap->getContext()); - $objectManager->injectAllSettings($configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS)); $objectManager->setObjects($objectConfigurationCache->get('objects')); foreach ($bootstrap->getEarlyInstances() as $objectName => $instance) { diff --git a/Neos.Flow/Classes/Monitor/FileMonitor.php b/Neos.Flow/Classes/Monitor/FileMonitor.php index 948e8ce772..87d5813260 100644 --- a/Neos.Flow/Classes/Monitor/FileMonitor.php +++ b/Neos.Flow/Classes/Monitor/FileMonitor.php @@ -91,7 +91,7 @@ class FileMonitor * @param string $identifier Name of this specific file monitor - will be used in the signals emitted by this monitor. * @api */ - public function __construct($identifier) + public function __construct(string $identifier) { $this->identifier = $identifier; } diff --git a/Neos.Flow/Classes/Mvc/DispatchMiddleware.php b/Neos.Flow/Classes/Mvc/DispatchMiddleware.php index 318b3702eb..2c3fec090d 100644 --- a/Neos.Flow/Classes/Mvc/DispatchMiddleware.php +++ b/Neos.Flow/Classes/Mvc/DispatchMiddleware.php @@ -25,11 +25,7 @@ */ class DispatchMiddleware implements MiddlewareInterface { - /** - * @Flow\Inject(lazy=false) - * @var Dispatcher - */ - protected $dispatcher; + public function __construct(protected Dispatcher $dispatcher) {} /** * Create an action request from stored route match values and dispatch to that diff --git a/Neos.Flow/Classes/Mvc/ViewConfigurationManager.php b/Neos.Flow/Classes/Mvc/ViewConfigurationManager.php index 3ea598ec72..68ecbd5d37 100644 --- a/Neos.Flow/Classes/Mvc/ViewConfigurationManager.php +++ b/Neos.Flow/Classes/Mvc/ViewConfigurationManager.php @@ -28,22 +28,11 @@ */ class ViewConfigurationManager { - /** - * @var VariableFrontend - */ - protected $cache; - - /** - * @Flow\Inject - * @var ConfigurationManager - */ - protected $configurationManager; - - /** - * @Flow\Inject - * @var CompilingEvaluator - */ - protected $eelEvaluator; + public function __construct( + protected ConfigurationManager $configurationManager, + protected CompilingEvaluator $eelEvaluator, + protected VariableFrontend $cache + ) {} /** * This method walks through the view configuration and applies diff --git a/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php b/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php index 8f678fe7ee..4eb878472a 100644 --- a/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php +++ b/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php @@ -18,6 +18,7 @@ use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException; use Neos\Flow\ObjectManagement\Configuration\Configuration; use Neos\Flow\ObjectManagement\Configuration\ConfigurationBuilder; +use Neos\Flow\ObjectManagement\Configuration\ConfigurationParser; use Neos\Flow\ObjectManagement\Configuration\ConfigurationProperty as Property; use Neos\Flow\Annotations as Flow; use Neos\Flow\ObjectManagement\Exception\InvalidObjectConfigurationException; @@ -136,10 +137,12 @@ public function initialize(array $packages): void $rawCustomObjectConfigurations = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_OBJECTS); - $configurationBuilder = new ConfigurationBuilder(); - $configurationBuilder->injectReflectionService($this->reflectionService); - $configurationBuilder->injectLogger($this->logger); - $configurationBuilder->injectExcludeClassesFromConstructorAutowiring($this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow.object.dependencyInjection.excludeClassesFromConstructorAutowiring')); + $configurationBuilder = new ConfigurationBuilder( + $this->reflectionService, + new ConfigurationParser($this->reflectionService), + $this->logger, + $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow.object.dependencyInjection.excludeClassesFromConstructorAutowiring') + ); $this->objectConfigurations = $configurationBuilder->buildObjectConfigurations($this->registeredClassNames, $rawCustomObjectConfigurations); @@ -214,18 +217,19 @@ public function getClassNamesByScope(int $scope): array */ protected function registerClassFiles(array $packages): array { + $allSettings = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS); $includeClassesConfiguration = []; - if (isset($this->allSettings['Neos']['Flow']['object']['includeClasses'])) { - if (!is_array($this->allSettings['Neos']['Flow']['object']['includeClasses'])) { + if (isset($allSettings['Neos']['Flow']['object']['includeClasses'])) { + if (!is_array($allSettings['Neos']['Flow']['object']['includeClasses'])) { throw new InvalidConfigurationTypeException('The setting "Neos.Flow.object.includeClasses" is invalid, it must be an array if set. Check the syntax in the YAML file.', 1422357285); } - $includeClassesConfiguration = $this->allSettings['Neos']['Flow']['object']['includeClasses']; + $includeClassesConfiguration = $allSettings['Neos']['Flow']['object']['includeClasses']; } $availableClassNames = ['' => ['DateTime']]; - $shouldRegisterFunctionalTestClasses = (bool)($this->allSettings['Neos']['Flow']['object']['registerFunctionalTestClasses'] ?? false); + $shouldRegisterFunctionalTestClasses = (bool)($allSettings['Neos']['Flow']['object']['registerFunctionalTestClasses'] ?? false); foreach ($packages as $packageKey => $package) { $packageType = (string)$package->getComposerManifest('type'); @@ -344,6 +348,21 @@ protected function buildObjectsArray(): array } } } + if ($objectConfiguration->getAutowiring() === Configuration::AUTOWIRING_MODE_ON) { + $constructorArguments = $objectConfiguration->getArguments(); + foreach ($constructorArguments as $index => $argument) { + if ($argument === null) { + $objects[$objectName][self::KEY_CONSTRUCTOR_ARGUMENTS][$index] = null; + continue; + } + $objects[$objectName][self::KEY_CONSTRUCTOR_ARGUMENTS][$index] = [ + self::KEY_ARGUMENT_TYPE => $argument->getType(), + self::KEY_ARGUMENT_VALUE => $argument->getValue(), + 'wm' => $argument->getAutowiring() + ]; + } + } + } $this->configurationCache->set('objects', $objects); return $objects; @@ -381,9 +400,6 @@ public function get($objectName, ...$constructorArguments): object return $this->objects[$objectName][self::KEY_INSTANCE]; } - if (isset($this->objectConfigurations[$objectName]) && count($this->objectConfigurations[$objectName]->getArguments()) > 0) { - throw new Exception\CannotBuildObjectException('Cannot build object "' . $objectName . '" because constructor injection is not available in the compile time Object Manager. Refactor your code to use setter injection instead. Configuration source: ' . $this->objectConfigurations[$objectName]->getConfigurationSourceHint() . '. Build stack: ' . implode(', ', $this->objectNameBuildStack), 1297090026); - } if (!isset($this->objects[$objectName])) { throw new Exception\UnknownObjectException('Cannot build object "' . $objectName . '" because it is unknown to the compile time Object Manager.', 1301477694); } diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php b/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php index bb24986041..5749869fb7 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php @@ -21,97 +21,97 @@ */ class Configuration { - const AUTOWIRING_MODE_OFF = 0; - const AUTOWIRING_MODE_ON = 1; + public const AUTOWIRING_MODE_OFF = 0; + public const AUTOWIRING_MODE_ON = 1; - const SCOPE_PROTOTYPE = 1; - const SCOPE_SINGLETON = 2; - const SCOPE_SESSION = 3; + public const SCOPE_PROTOTYPE = 1; + public const SCOPE_SINGLETON = 2; + public const SCOPE_SESSION = 3; /** * Name of the object * @var string $objectName */ - protected $objectName; + protected string $objectName; /** * Name of the class the object is based on - * @var string $className + * @var class-string $className */ - protected $className; + protected string $className; /** * Key of the package the specified object is part of * @var string */ - protected $packageKey; + protected string $packageKey = ''; /** * If set, specifies the factory object name used to create this object * @var string */ - protected $factoryObjectName = ''; + protected string $factoryObjectName = ''; /** * Name of the factory method. * @var string */ - protected $factoryMethodName = ''; + protected string $factoryMethodName = ''; /** * Arguments of the factory method - * @var array + * @var array */ - protected $factoryArguments = []; + protected array $factoryArguments = []; /** * @var int */ - protected $scope = self::SCOPE_PROTOTYPE; + protected int $scope = self::SCOPE_PROTOTYPE; /** * Arguments of the constructor detected by reflection - * @var array + * @var array */ - protected $arguments = []; + protected array $arguments = []; /** * Array of properties which are injected into the object - * @var array + * @var array */ - protected $properties = []; + protected array $properties = []; /** * Mode of the autowiring feature. One of the AUTOWIRING_MODE_* constants * @var integer */ - protected $autowiring = self::AUTOWIRING_MODE_ON; + protected int $autowiring = self::AUTOWIRING_MODE_ON; /** * Name of the method to call during the initialization of the object (after dependencies are injected) * @var string */ - protected $lifecycleInitializationMethodName = 'initializeObject'; + protected string $lifecycleInitializationMethodName = 'initializeObject'; /** * Name of the method to call during the shutdown of the framework * @var string */ - protected $lifecycleShutdownMethodName = 'shutdownObject'; + protected string $lifecycleShutdownMethodName = 'shutdownObject'; /** * Information about where this configuration has been created. Used in error messages to make debugging easier. * @var string */ - protected $configurationSourceHint = '< unknown >'; + protected string $configurationSourceHint = '< unknown >'; /** * The constructor * * @param string $objectName The unique identifier of the object - * @param string $className Name of the class which provides the functionality of this object + * @param class-string $className Name of the class which provides the functionality of this object */ - public function __construct($objectName, $className = null) + public function __construct(string $objectName, string $className) { $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); if (isset($backtrace[1]['object'])) { @@ -121,7 +121,7 @@ public function __construct($objectName, $className = null) } $this->objectName = $objectName; - $this->className = ($className === null ? $objectName : $className); + $this->className = $className; } /** @@ -130,10 +130,9 @@ public function __construct($objectName, $className = null) * @param string $objectName * @return void */ - public function setObjectName($objectName) + public function setObjectName(string $objectName): void { $this->objectName = $objectName; - ; } /** @@ -141,7 +140,7 @@ public function setObjectName($objectName) * * @return string object name */ - public function getObjectName() + public function getObjectName(): string { return $this->objectName; } @@ -149,10 +148,10 @@ public function getObjectName() /** * Setter function for property "className" * - * @param string $className Name of the class which provides the functionality for this object + * @param class-string $className Name of the class which provides the functionality for this object * @return void */ - public function setClassName($className) + public function setClassName(string $className): void { $this->className = $className; } @@ -160,9 +159,9 @@ 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() + public function getClassName(): string { return $this->className; } @@ -173,7 +172,7 @@ public function getClassName() * @param string $packageKey Key of the package this object is part of * @return void */ - public function setPackageKey($packageKey) + public function setPackageKey(string $packageKey): void { $this->packageKey = $packageKey; } @@ -183,7 +182,7 @@ public function setPackageKey($packageKey) * * @return string Key of the package this object is part of */ - public function getPackageKey() + public function getPackageKey(): string { return $this->packageKey; } @@ -191,10 +190,10 @@ public function getPackageKey() /** * Sets the class name of a factory which is in charge of instantiating this object * - * @param string $objectName Valid object name of a factory + * @param class-string $objectName Valid object name of a factory * @return void */ - public function setFactoryObjectName($objectName) + public function setFactoryObjectName(string $objectName): void { $this->factoryObjectName = $objectName; if ($this->factoryMethodName === '') { @@ -207,9 +206,9 @@ public function setFactoryObjectName($objectName) /** * Returns the class name of the factory for this object, if any * - * @return string The factory class name + * @return class-string The factory class name */ - public function getFactoryObjectName() + public function getFactoryObjectName(): string { return $this->factoryObjectName; } @@ -221,9 +220,9 @@ public function getFactoryObjectName() * @return void * @throws \InvalidArgumentException */ - public function setFactoryMethodName($methodName) + public function setFactoryMethodName(string $methodName): void { - if (!is_string($methodName) || $methodName === '') { + if ($methodName === '') { throw new \InvalidArgumentException('No valid factory method name specified.', 1229700126); } $this->factoryMethodName = $methodName; @@ -234,17 +233,15 @@ public function setFactoryMethodName($methodName) * * @return string The factory method name */ - public function getFactoryMethodName() + public function getFactoryMethodName(): string { return $this->factoryMethodName; } /** * Returns true if factoryObjectName or factoryMethodName are defined. - * - * @return boolean */ - public function isCreatedByFactory() + public function isCreatedByFactory(): bool { return ($this->factoryObjectName !== '' || $this->factoryMethodName !== ''); } @@ -255,7 +252,7 @@ public function isCreatedByFactory() * @param integer $scope Name of the scope * @return void */ - public function setScope($scope) + public function setScope(int $scope): void { $this->scope = $scope; } @@ -265,7 +262,7 @@ public function setScope($scope) * * @return int The scope, one of the SCOPE constants */ - public function getScope() + public function getScope(): int { return $this->scope; } @@ -276,7 +273,7 @@ public function getScope() * @param integer $autowiring One of the AUTOWIRING_MODE_* constants * @return void */ - public function setAutowiring($autowiring) + public function setAutowiring(int $autowiring): void { $this->autowiring = $autowiring; } @@ -286,7 +283,7 @@ public function setAutowiring($autowiring) * * @return integer Value of one of the AUTOWIRING_MODE_* constants */ - public function getAutowiring() + public function getAutowiring(): int { return $this->autowiring; } @@ -297,7 +294,7 @@ public function getAutowiring() * @param string $lifecycleInitializationMethodName Name of the method to call after setter injection * @return void */ - public function setLifecycleInitializationMethodName($lifecycleInitializationMethodName) + public function setLifecycleInitializationMethodName(string $lifecycleInitializationMethodName): void { $this->lifecycleInitializationMethodName = $lifecycleInitializationMethodName; } @@ -307,7 +304,7 @@ public function setLifecycleInitializationMethodName($lifecycleInitializationMet * * @return string The name of the initialization method */ - public function getLifecycleInitializationMethodName() + public function getLifecycleInitializationMethodName(): string { return $this->lifecycleInitializationMethodName; } @@ -318,7 +315,7 @@ public function getLifecycleInitializationMethodName() * @param string $lifecycleShutdownMethodName Name of the method to call during shutdown of the framework * @return void */ - public function setLifecycleShutdownMethodName($lifecycleShutdownMethodName) + public function setLifecycleShutdownMethodName(string $lifecycleShutdownMethodName): void { $this->lifecycleShutdownMethodName = $lifecycleShutdownMethodName; } @@ -328,7 +325,7 @@ public function setLifecycleShutdownMethodName($lifecycleShutdownMethodName) * * @return string The name of the shutdown method */ - public function getLifecycleShutdownMethodName() + public function getLifecycleShutdownMethodName(): string { return $this->lifecycleShutdownMethodName; } @@ -337,22 +334,14 @@ public function getLifecycleShutdownMethodName() * Setter function for injection properties. If an empty array is passed to this * method, all (possibly) defined properties are removed from the configuration. * - * @param array $properties Array of ConfigurationProperty - * @throws InvalidConfigurationException + * @param ConfigurationProperty[] $properties Array of ConfigurationProperty * @return void */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { - if ($properties === []) { - $this->properties = []; - } else { - foreach ($properties as $value) { - if ($value instanceof ConfigurationProperty) { - $this->setProperty($value); - } else { - throw new InvalidConfigurationException(sprintf('Only ConfigurationProperty instances are allowed, "%s" given', is_object($value) ? get_class($value) : gettype($value)), 1449217567); - } - } + $this->properties = []; + foreach ($properties as $value) { + $this->setProperty($value); } } @@ -361,7 +350,7 @@ public function setProperties(array $properties) * * @return array */ - public function getProperties() + public function getProperties(): array { return $this->properties; } @@ -372,7 +361,7 @@ public function getProperties() * @param ConfigurationProperty $property * @return void */ - public function setProperty(ConfigurationProperty $property) + public function setProperty(ConfigurationProperty $property): void { $this->properties[$property->getName()] = $property; } @@ -385,18 +374,11 @@ public function setProperty(ConfigurationProperty $property) * @throws InvalidConfigurationException * @return void */ - public function setArguments(array $arguments) + public function setArguments(array $arguments): void { - if ($arguments === []) { - $this->arguments = []; - } else { - foreach ($arguments as $argument) { - if ($argument instanceof ConfigurationArgument) { - $this->setArgument($argument); - } else { - throw new InvalidConfigurationException(sprintf('Only ConfigurationArgument instances are allowed, "%s" given', is_object($argument) ? get_class($argument) : gettype($argument)), 1449217803); - } - } + $this->arguments = []; + foreach ($arguments as $argument) { + $this->setArgument($argument); } } @@ -406,7 +388,7 @@ public function setArguments(array $arguments) * @param ConfigurationArgument $argument The argument * @return void */ - public function setArgument(ConfigurationArgument $argument) + public function setArgument(ConfigurationArgument $argument): void { $this->arguments[$argument->getIndex()] = $argument; } @@ -416,7 +398,7 @@ public function setArgument(ConfigurationArgument $argument) * * @return array A sorted array of ConfigurationArgument objects with the argument position as index */ - public function getArguments() + public function getArguments(): array { if (count($this->arguments) < 1) { return []; @@ -438,7 +420,7 @@ public function getArguments() * @param ConfigurationArgument $argument The argument * @return void */ - public function setFactoryArgument(ConfigurationArgument $argument) + public function setFactoryArgument(ConfigurationArgument $argument): void { $this->factoryArguments[$argument->getIndex()] = $argument; } @@ -448,7 +430,7 @@ public function setFactoryArgument(ConfigurationArgument $argument) * * @return array A sorted array of ConfigurationArgument objects with the argument position as index */ - public function getFactoryArguments() + public function getFactoryArguments(): array { if (count($this->factoryArguments) < 1) { return []; @@ -470,7 +452,7 @@ public function getFactoryArguments() * @param string $hint The hint - e.g. the filename of the configuration file * @return void */ - public function setConfigurationSourceHint($hint) + public function setConfigurationSourceHint(string $hint): void { $this->configurationSourceHint = $hint; } @@ -480,7 +462,7 @@ public function setConfigurationSourceHint($hint) * * @return string The hint - e.g. the filename of the configuration file */ - public function getConfigurationSourceHint() + public function getConfigurationSourceHint(): string { return $this->configurationSourceHint; } diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationArgument.php b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationArgument.php index 89a1bb12f6..d4d46f21ed 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationArgument.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationArgument.php @@ -18,67 +18,32 @@ * * @Flow\Proxy(false) */ -class ConfigurationArgument +final readonly class ConfigurationArgument { - const ARGUMENT_TYPES_STRAIGHTVALUE = 0; - const ARGUMENT_TYPES_OBJECT = 1; - const ARGUMENT_TYPES_SETTING = 2; - - /** - * The position of the constructor argument. Counting starts at "1". - * @var integer - */ - protected $index; - - /** - * @var mixed The argument's value - */ - protected $value; - - /** - * Argument type, one of the ARGUMENT_TYPES_* constants - * @var integer - */ - protected $type; - - /** - * @var integer - */ - protected $autowiring = Configuration::AUTOWIRING_MODE_ON; + public const ARGUMENT_TYPES_STRAIGHTVALUE = 0; + public const ARGUMENT_TYPES_OBJECT = 1; + public const ARGUMENT_TYPES_SETTING = 2; /** * Constructor - sets the index, value and type of the argument * - * @param string $index Index of the argument + * @param int $index Index of the argument * @param mixed $value Value of the argument * @param integer $type Type of the argument - one of the argument_TYPE_* constants */ - public function __construct($index, $value, $type = self::ARGUMENT_TYPES_STRAIGHTVALUE) - { - $this->set($index, $value, $type); - } - - /** - * Sets the index, value, type of the argument and object configuration - * - * @param integer $index Index of the argument (counting starts at "1") - * @param mixed $value Value of the argument - * @param integer $type Type of the argument - one of the ARGUMENT_TYPE_* constants - * @return void - */ - public function set($index, $value, $type = self::ARGUMENT_TYPES_STRAIGHTVALUE) - { - $this->index = $index; - $this->value = $value; - $this->type = $type; - } + public function __construct( + protected int $index, + protected mixed $value, + protected int $type = self::ARGUMENT_TYPES_STRAIGHTVALUE, + protected int $autowiring = Configuration::AUTOWIRING_MODE_ON + ) {} /** * Returns the index (position) of the argument * * @return int Index of the argument */ - public function getIndex() + public function getIndex(): int { return $this->index; } @@ -88,7 +53,7 @@ public function getIndex() * * @return mixed Value of the argument */ - public function getValue() + public function getValue(): mixed { return $this->value; } @@ -98,28 +63,17 @@ public function getValue() * * @return integer Type of the argument - one of the ARGUMENT_TYPES_* constants */ - public function getType() + public function getType(): int { return $this->type; } - /** - * Sets autowiring for this argument - * - * @param integer $autowiring One of the Configuration::AUTOWIRING_MODE_* constants - * @return void - */ - public function setAutowiring($autowiring) - { - $this->autowiring = $autowiring; - } - /** * Returns the autowiring mode for this argument * * @return integer Value of one of the Configuration::AUTOWIRING_MODE_* constants */ - public function getAutowiring() + public function getAutowiring(): int { return $this->autowiring; } diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php index f540adb8d0..4e1c1f87e0 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php @@ -16,11 +16,14 @@ use Neos\Flow\Annotations\InjectCache; use Neos\Flow\Annotations\InjectConfiguration; use Neos\Flow\Configuration\ConfigurationManager; +use Neos\Flow\Configuration\Exception\InvalidConfigurationException; use Neos\Flow\ObjectManagement\Exception as ObjectException; use Neos\Flow\ObjectManagement\Exception\InvalidObjectConfigurationException; use Neos\Flow\ObjectManagement\Exception\UnknownClassException; use Neos\Flow\ObjectManagement\Exception\UnresolvedDependenciesException; use Neos\Flow\ObjectManagement\ObjectManager; +use Neos\Flow\Reflection\Exception\ClassLoadingForReflectionFailedException; +use Neos\Flow\Reflection\Exception\InvalidClassException; use Neos\Flow\Reflection\ReflectionService; use Psr\Log\LoggerInterface; @@ -32,50 +35,20 @@ * @Flow\Scope("singleton") * @Flow\Proxy(false) */ -class ConfigurationBuilder +readonly class ConfigurationBuilder { - /** - * @var ReflectionService - */ - protected $reflectionService; - - /** - * @var LoggerInterface - */ - protected $logger; - - /** - * An array of object names for which constructor injection autowiring should be disabled. - * Note that the object names are regular expressions. - * - * @var array - */ - protected array $excludeClassesFromConstructorAutowiring = []; - /** * @param ReflectionService $reflectionService - * @return void - */ - public function injectReflectionService(ReflectionService $reflectionService): void - { - $this->reflectionService = $reflectionService; - } - - /** - * Injects the (system) logger based on PSR-3. - * * @param LoggerInterface $logger - * @return void + * @param array $excludeClassesFromConstructorAutowiring An array of object names for which constructor injection autowiring should be disabled + * Note that the object names are regular expressions. */ - public function injectLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } - - public function injectExcludeClassesFromConstructorAutowiring(array $excludeClassesFromConstructorAutowiring): void - { - $this->excludeClassesFromConstructorAutowiring = $excludeClassesFromConstructorAutowiring; - } + public function __construct( + protected ReflectionService $reflectionService, + protected ConfigurationParser $configurationParser, + protected LoggerInterface $logger, + protected array $excludeClassesFromConstructorAutowiring = [] + ) {} /** * Traverses through the given class and interface names and builds a base object configuration @@ -83,12 +56,18 @@ public function injectExcludeClassesFromConstructorAutowiring(array $excludeClas * into the overall configuration. Finally autowires dependencies of arguments and properties * which can be resolved automatically. * - * @param array $availableClassAndInterfaceNamesByPackage An array of available class names, grouped by package key + * @param array> $availableClassAndInterfaceNamesByPackage An array of available class names, grouped by package key * @param array $rawObjectConfigurationsByPackages An array of package keys and their raw (ie. unparsed) object configurations * @return array Object configurations + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException * @throws InvalidObjectConfigurationException + * @throws ObjectException + * @throws UnknownClassException + * @throws UnresolvedDependenciesException + * @throws \ReflectionException */ - public function buildObjectConfigurations(array $availableClassAndInterfaceNamesByPackage, array $rawObjectConfigurationsByPackages) + public function buildObjectConfigurations(array $availableClassAndInterfaceNamesByPackage, array $rawObjectConfigurationsByPackages): array { $objectConfigurations = []; $interfaceNames = []; @@ -101,6 +80,7 @@ public function buildObjectConfigurations(array $availableClassAndInterfaceNames continue; } + $implementationClassName = $classOrInterfaceName; if (interface_exists($classOrInterfaceName)) { $interfaceName = $classOrInterfaceName; $implementationClassName = $this->reflectionService->getDefaultImplementationClassNameForInterface($interfaceName); @@ -111,30 +91,29 @@ public function buildObjectConfigurations(array $availableClassAndInterfaceNames throw new InvalidObjectConfigurationException(sprintf('Scope annotations in interfaces don\'t have any effect, therefore you better remove it from %s in order to avoid confusion.', $interfaceName), 1299095595); } $interfaceNames[$interfaceName] = true; - } else { - $implementationClassName = $classOrInterfaceName; } $rawObjectConfiguration = ['className' => $implementationClassName]; $rawObjectConfiguration = $this->enhanceRawConfigurationWithAnnotationOptions($classOrInterfaceName, $rawObjectConfiguration); - $objectConfigurations[$objectName] = $this->parseConfigurationArray($objectName, $rawObjectConfiguration, 'automatically registered class'); + $objectConfigurations[$objectName] = $this->configurationParser->parseConfigurationArray($objectName, $rawObjectConfiguration, 'automatically registered class'); $objectConfigurations[$objectName]->setPackageKey($packageKey); } } foreach ($rawObjectConfigurationsByPackages as $packageKey => $rawObjectConfigurations) { foreach ($rawObjectConfigurations as $objectName => $rawObjectConfiguration) { + /** @var class-string $objectName */ $objectName = str_replace('_', '\\', $objectName); if (!is_array($rawObjectConfiguration)) { throw new InvalidObjectConfigurationException('Configuration of object "' . $objectName . '" in package "' . $packageKey . '" is not an array, please check your Objects.yaml for syntax errors.', 1295954338); } - $existingObjectConfiguration = (isset($objectConfigurations[$objectName])) ? $objectConfigurations[$objectName] : null; + $existingObjectConfiguration = $objectConfigurations[$objectName] ?? null; if (isset($rawObjectConfiguration['className'])) { $rawObjectConfiguration = $this->enhanceRawConfigurationWithAnnotationOptions($rawObjectConfiguration['className'], $rawObjectConfiguration); } // Virtual objects are determined by a colon ":" in the name (e.g. "Some.Package:Some.Virtual.Object") - $isVirtualObject = strpos($objectName, ':') !== false; + $isVirtualObject = str_contains($objectName, ':') !== false; if ($isVirtualObject && empty($rawObjectConfiguration['className'])) { throw new InvalidObjectConfigurationException(sprintf('Missing className for virtual object configuration "%s" of package %s. Please check your Objects.yaml.', $objectName, $packageKey), 1585758850); } @@ -149,7 +128,7 @@ public function buildObjectConfigurations(array $availableClassAndInterfaceNames } $rawObjectConfiguration['arguments'] = $newArguments; } - $newObjectConfiguration = $this->parseConfigurationArray($objectName, $rawObjectConfiguration, 'configuration of package ' . $packageKey . ', definition for object "' . $objectName . '"', $existingObjectConfiguration); + $newObjectConfiguration = $this->configurationParser->parseConfigurationArray($objectName, $rawObjectConfiguration, 'configuration of package ' . $packageKey . ', definition for object "' . $objectName . '"', $existingObjectConfiguration); if (!$isVirtualObject && !isset($objectConfigurations[$objectName]) && !interface_exists($objectName, true) && !class_exists($objectName, false)) { throw new InvalidObjectConfigurationException('Tried to configure unknown object "' . $objectName . '" in package "' . $packageKey . '". Please check your Objects.yaml.', 1184926175); @@ -166,7 +145,7 @@ public function buildObjectConfigurations(array $availableClassAndInterfaceNames } $objectConfigurations[$objectName] = $newObjectConfiguration; - if ($objectConfigurations[$objectName]->getPackageKey() === null) { + if ($objectConfigurations[$objectName]->getPackageKey() === '') { $objectConfigurations[$objectName]->setPackageKey($packageKey); } } @@ -210,187 +189,6 @@ protected function enhanceRawConfigurationWithAnnotationOptions($className, arra return $rawObjectConfiguration; } - /** - * Builds an object configuration object from a generic configuration container. - * - * @param string $objectName Name of the object - * @param array $rawConfigurationOptions The configuration array with options for the object configuration - * @param string $configurationSourceHint A human readable hint on the original source of the configuration (for troubleshooting) - * @param Configuration $existingObjectConfiguration If set, this object configuration object will be used instead of creating a fresh one - * @return Configuration The object configuration object - * @throws InvalidObjectConfigurationException if errors occurred during parsing - */ - protected function parseConfigurationArray($objectName, array $rawConfigurationOptions, $configurationSourceHint = '', $existingObjectConfiguration = null) - { - $className = $rawConfigurationOptions['className'] ?? $objectName; - $objectConfiguration = ($existingObjectConfiguration instanceof Configuration) ? $existingObjectConfiguration : new Configuration($objectName, $className); - $objectConfiguration->setConfigurationSourceHint($configurationSourceHint); - - foreach ($rawConfigurationOptions as $optionName => $optionValue) { - switch ($optionName) { - case 'scope': - $objectConfiguration->setScope($this->parseScope($optionValue)); - break; - case 'properties': - if (is_array($optionValue)) { - foreach ($optionValue as $propertyName => $propertyValue) { - if (array_key_exists('value', $propertyValue)) { - $property = new ConfigurationProperty($propertyName, $propertyValue['value'], ConfigurationProperty::PROPERTY_TYPES_STRAIGHTVALUE); - } elseif (array_key_exists('object', $propertyValue)) { - $property = $this->parsePropertyOfTypeObject($propertyName, $propertyValue['object'], $objectConfiguration); - } elseif (array_key_exists('setting', $propertyValue)) { - $property = new ConfigurationProperty($propertyName, ['type' => ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'path' => $propertyValue['setting']], ConfigurationProperty::PROPERTY_TYPES_CONFIGURATION); - } else { - throw new InvalidObjectConfigurationException('Invalid configuration syntax. Expecting "value", "object" or "setting" as value for property "' . $propertyName . '", instead found "' . (is_array($propertyValue) ? implode(', ', array_keys($propertyValue)) : $propertyValue) . '" (source: ' . $objectConfiguration->getConfigurationSourceHint() . ')', 1230563249); - } - $objectConfiguration->setProperty($property); - } - } - break; - case 'arguments': - if (is_array($optionValue)) { - foreach ($optionValue as $argumentName => $argumentValue) { - if (array_key_exists('value', $argumentValue)) { - $argument = new ConfigurationArgument($argumentName, $argumentValue['value'], ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE); - } elseif (array_key_exists('object', $argumentValue)) { - $argument = $this->parseArgumentOfTypeObject($argumentName, $argumentValue['object'], $configurationSourceHint); - } elseif (array_key_exists('setting', $argumentValue)) { - $argument = new ConfigurationArgument($argumentName, $argumentValue['setting'], ConfigurationArgument::ARGUMENT_TYPES_SETTING); - } else { - throw new InvalidObjectConfigurationException('Invalid configuration syntax. Expecting "value", "object" or "setting" as value for argument "' . $argumentName . '", instead found "' . (is_array($argumentValue) ? implode(', ', array_keys($argumentValue)) : $argumentValue) . '" (source: ' . $objectConfiguration->getConfigurationSourceHint() . ')', 1230563250); - } - if (isset($rawConfigurationOptions['factoryObjectName']) || isset($rawConfigurationOptions['factoryMethodName'])) { - $objectConfiguration->setFactoryArgument($argument); - } else { - $objectConfiguration->setArgument($argument); - } - } - } - break; - case 'className': - case 'factoryObjectName': - case 'factoryMethodName': - case 'lifecycleInitializationMethodName': - case 'lifecycleShutdownMethodName': - $methodName = 'set' . ucfirst($optionName); - $objectConfiguration->$methodName(trim((string)$optionValue)); - break; - case 'autowiring': - $objectConfiguration->setAutowiring(self::parseAutowiring($optionValue)); - break; - default: - throw new InvalidObjectConfigurationException('Invalid configuration option "' . $optionName . '" (source: ' . $objectConfiguration->getConfigurationSourceHint() . ')', 1167574981); - } - } - return $objectConfiguration; - } - - /** - * Parses the value of the option "scope" - * - * @param string $value Value of the option - * @return integer The scope translated into a Configuration::SCOPE_* constant - * @throws InvalidObjectConfigurationException if an invalid scope has been specified - */ - protected function parseScope($value) - { - switch ($value) { - case 'singleton': - return Configuration::SCOPE_SINGLETON; - case 'prototype': - return Configuration::SCOPE_PROTOTYPE; - case 'session': - return Configuration::SCOPE_SESSION; - default: - throw new InvalidObjectConfigurationException('Invalid scope "' . $value . '"', 1167574991); - } - } - - /** - * Parses the value of the option "autowiring" - * - * @param mixed $value Value of the option - * @return integer The autowiring option translated into one of Configuration::AUTOWIRING_MODE_* - * @throws InvalidObjectConfigurationException if an invalid option has been specified - */ - protected static function parseAutowiring($value) - { - switch ($value) { - case true: - case Configuration::AUTOWIRING_MODE_ON: - return Configuration::AUTOWIRING_MODE_ON; - case false: - case Configuration::AUTOWIRING_MODE_OFF: - return Configuration::AUTOWIRING_MODE_OFF; - default: - throw new InvalidObjectConfigurationException('Invalid autowiring declaration', 1283866757); - } - } - - /** - * Parses the configuration for properties of type OBJECT - * - * @param string $propertyName Name of the property - * @param mixed $objectNameOrConfiguration Value of the "object" section of the property configuration - either a string or an array - * @param Configuration $parentObjectConfiguration The Configuration object this property belongs to - * @return ConfigurationProperty A configuration property of type object - * @throws InvalidObjectConfigurationException - */ - protected function parsePropertyOfTypeObject($propertyName, $objectNameOrConfiguration, Configuration $parentObjectConfiguration) - { - if (is_array($objectNameOrConfiguration)) { - if (isset($objectNameOrConfiguration['name'])) { - $objectName = $objectNameOrConfiguration['name']; - unset($objectNameOrConfiguration['name']); - } else { - if (isset($objectNameOrConfiguration['factoryObjectName']) || isset($objectNameOrConfiguration['factoryMethodName'])) { - $objectName = null; - } else { - $annotations = $this->reflectionService->getPropertyTagValues($parentObjectConfiguration->getClassName(), $propertyName, 'var'); - if (count($annotations) !== 1) { - throw new InvalidObjectConfigurationException(sprintf('Object %s (%s), for property "%s", contains neither object name, nor factory object name, and nor is the property properly @var - annotated.', $parentObjectConfiguration->getClassName(), $parentObjectConfiguration->getConfigurationSourceHint(), $propertyName), 1297097815); - } - $objectName = $annotations[0]; - } - } - $objectConfiguration = $this->parseConfigurationArray($objectName, $objectNameOrConfiguration, $parentObjectConfiguration->getConfigurationSourceHint() . ', property "' . $propertyName . '"'); - $property = new ConfigurationProperty($propertyName, $objectConfiguration, ConfigurationProperty::PROPERTY_TYPES_OBJECT); - } else { - $property = new ConfigurationProperty($propertyName, $objectNameOrConfiguration, ConfigurationProperty::PROPERTY_TYPES_OBJECT); - } - return $property; - } - - /** - * Parses the configuration for arguments of type OBJECT - * - * @param string $argumentName Name of the argument - * @param mixed $objectNameOrConfiguration Value of the "object" section of the argument configuration - either a string or an array - * @param string $configurationSourceHint A human readable hint on the original source of the configuration (for troubleshooting) - * @return ConfigurationArgument A configuration argument of type object - * @throws InvalidObjectConfigurationException - */ - protected function parseArgumentOfTypeObject($argumentName, $objectNameOrConfiguration, $configurationSourceHint) - { - if (is_array($objectNameOrConfiguration)) { - if (isset($objectNameOrConfiguration['name'])) { - $objectName = $objectNameOrConfiguration['name']; - unset($objectNameOrConfiguration['name']); - } else { - if (isset($objectNameOrConfiguration['factoryObjectName']) || isset($objectNameOrConfiguration['factoryMethodName'])) { - $objectName = null; - } else { - throw new InvalidObjectConfigurationException('Object configuration for argument "' . $argumentName . '" contains neither object name nor factory object or method name in ' . $configurationSourceHint, 1417431742); - } - } - $objectConfiguration = $this->parseConfigurationArray($objectName, $objectNameOrConfiguration, $configurationSourceHint . ', argument "' . $argumentName . '"'); - $argument = new ConfigurationArgument($argumentName, $objectConfiguration, ConfigurationArgument::ARGUMENT_TYPES_OBJECT); - } else { - $argument = new ConfigurationArgument($argumentName, $objectNameOrConfiguration, ConfigurationArgument::ARGUMENT_TYPES_OBJECT); - } - return $argument; - } - /** * Creates a "virtual object configuration" for factory arguments, turning: * @@ -413,16 +211,14 @@ protected function parseArgumentOfTypeObject($argumentName, $objectNameOrConfigu * factoryObjectName: 'Some\Other\Factory\Class' * * - * @param array &$objectConfigurations + * @param array &$objectConfigurations * @return void */ - protected function wireFactoryArguments(array &$objectConfigurations) + protected function wireFactoryArguments(array $objectConfigurations): void { - /** @var Configuration $objectConfiguration */ foreach ($objectConfigurations as $objectConfiguration) { - /** @var ConfigurationArgument $argument */ foreach ($objectConfiguration->getFactoryArguments() as $index => $argument) { - if ($argument === null || $argument->getType() !== ConfigurationArgument::ARGUMENT_TYPES_OBJECT) { + if ($argument->getType() !== ConfigurationArgument::ARGUMENT_TYPES_OBJECT) { continue; } $argumentValue = $argument->getValue(); @@ -431,11 +227,8 @@ protected function wireFactoryArguments(array &$objectConfigurations) } $argumentObjectName = $objectConfiguration->getObjectName() . ':argument:' . $index; $argumentValue->setObjectName($argumentObjectName); - if ($argumentValue->getClassName() === null) { - $argumentValue->setClassName(''); - } $objectConfigurations[$argumentObjectName] = $argument->getValue(); - $argument->set((int)$argument->getIndex(), $argumentObjectName, $argument->getType()); + $objectConfiguration->setFactoryArgument(new ConfigurationArgument($argument->getIndex(), $argumentObjectName, ConfigurationArgument::ARGUMENT_TYPES_OBJECT, $argument->getAutowiring())); } } } @@ -444,14 +237,18 @@ protected function wireFactoryArguments(array &$objectConfigurations) * If mandatory constructor arguments have not been defined yet, this function tries to autowire * them if possible. * - * @param array &$objectConfigurations + * @param array &$objectConfigurations * @return void + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException + * @throws InvalidObjectConfigurationException * @throws UnresolvedDependenciesException + * @throws InvalidConfigurationException + * @throws \ReflectionException */ protected function autowireArguments(array $objectConfigurations): void { foreach ($objectConfigurations as $objectConfiguration) { - /** @var Configuration $objectConfiguration */ $className = $objectConfiguration->getClassName(); if ($className === '') { continue; @@ -476,6 +273,7 @@ protected function autowireArguments(array $objectConfigurations): void } } + /** @var Flow\Autowiring $autowiringAnnotation */ $autowiringAnnotation = $this->reflectionService->getMethodAnnotation($className, '__construct', Flow\Autowiring::class); if ($autowiringAnnotation !== null && $autowiringAnnotation->enabled === false) { continue; @@ -497,14 +295,12 @@ protected function autowireArguments(array $objectConfigurations): void ConfigurationArgument::ARGUMENT_TYPES_SETTING ); } elseif ($parameterInformation['optional'] === true) { - $defaultValue = (isset($parameterInformation['defaultValue'])) ? $parameterInformation['defaultValue'] : null; - $arguments[$index] = new ConfigurationArgument($index, $defaultValue, ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE); - $arguments[$index]->setAutowiring(Configuration::AUTOWIRING_MODE_OFF); + $defaultValue = $parameterInformation['defaultValue'] ?? null; + $arguments[$index] = new ConfigurationArgument($index, $defaultValue, ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE, Configuration::AUTOWIRING_MODE_OFF); } elseif ($parameterInformation['class'] !== null && isset($objectConfigurations[$parameterInformation['class']])) { $arguments[$index] = new ConfigurationArgument($index, $parameterInformation['class'], ConfigurationArgument::ARGUMENT_TYPES_OBJECT); } elseif ($parameterInformation['allowsNull'] === true) { - $arguments[$index] = new ConfigurationArgument($index, null, ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE); - $arguments[$index]->setAutowiring(Configuration::AUTOWIRING_MODE_OFF); + $arguments[$index] = new ConfigurationArgument($index, null, ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE, Configuration::AUTOWIRING_MODE_OFF); } elseif (interface_exists($parameterInformation['class'])) { $debuggingHint = sprintf('No default implementation for the required interface %s was configured, therefore no specific class name could be used for this dependency. ', $parameterInformation['class']); } @@ -522,13 +318,16 @@ protected function autowireArguments(array $objectConfigurations): void /** * This function tries to find yet unmatched dependencies which need to be injected via "inject*" setter methods. * - * @param array &$objectConfigurations + * @param array &$objectConfigurations * @return void * @throws ObjectException if an injected property is private + * @throws UnknownClassException + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException + * @throws \ReflectionException */ - protected function autowireProperties(array &$objectConfigurations) + protected function autowireProperties(array $objectConfigurations): void { - /** @var Configuration $objectConfiguration */ foreach ($objectConfigurations as $objectConfiguration) { $className = $objectConfiguration->getClassName(); $properties = $objectConfiguration->getProperties(); @@ -548,14 +347,14 @@ protected function autowireProperties(array &$objectConfigurations) if (!is_array($classMethodNames)) { if (!class_exists($className)) { throw new UnknownClassException(sprintf('The class "%s" defined in the object configuration for object "%s", defined in package: %s, does not exist.', $className, $objectConfiguration->getObjectName(), $objectConfiguration->getPackageKey()), 1352371371); - } else { - throw new UnknownClassException(sprintf('Could not autowire properties of class "%s" because names of methods contained in that class could not be retrieved using get_class_methods().', $className), 1352386418); } + throw new UnknownClassException(sprintf('Could not autowire properties of class "%s" because names of methods contained in that class could not be retrieved using get_class_methods().', $className), 1352386418); } foreach ($classMethodNames as $methodName) { - if (isset($methodName[6]) && strpos($methodName, 'inject') === 0 && $methodName[6] === strtoupper($methodName[6])) { + if (isset($methodName[6]) && str_starts_with($methodName, 'inject') && $methodName[6] === strtoupper($methodName[6])) { $propertyName = lcfirst(substr($methodName, 6)); + /** @var Flow\Autowiring $autowiringAnnotation */ $autowiringAnnotation = $this->reflectionService->getMethodAnnotation($className, $methodName, Flow\Autowiring::class); if ($autowiringAnnotation !== null && $autowiringAnnotation->enabled === false) { continue; @@ -563,7 +362,7 @@ protected function autowireProperties(array &$objectConfigurations) if ($methodName === 'injectSettings') { $packageKey = $objectConfiguration->getPackageKey(); - if ($packageKey !== null) { + if ($packageKey !== '') { $properties[$propertyName] = new ConfigurationProperty($propertyName, ['type' => ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'path' => $packageKey], ConfigurationProperty::PROPERTY_TYPES_CONFIGURATION); } } else { @@ -642,17 +441,6 @@ protected function autowireProperties(array &$objectConfigurations) $properties[$propertyName] = new ConfigurationProperty($propertyName, ['identifier' => $injectCacheAnnotation->identifier], ConfigurationProperty::PROPERTY_TYPES_CACHE); } - foreach ($this->reflectionService->getPropertyNamesByAnnotation($className, InjectCache::class) as $propertyName) { - if ($this->reflectionService->isPropertyPrivate($className, $propertyName)) { - throw new ObjectException(sprintf('The property "%s" in class "%s" must not be private when annotated for cache injection.', $propertyName, $className), 1416765599); - } - if (array_key_exists($propertyName, $properties)) { - continue; - } - /** @var InjectCache $injectCacheAnnotation */ - $injectCacheAnnotation = $this->reflectionService->getPropertyAnnotation($className, $propertyName, InjectCache::class); - $properties[$propertyName] = new ConfigurationProperty($propertyName, ['identifier' => $injectCacheAnnotation->identifier], ConfigurationProperty::PROPERTY_TYPES_CACHE); - } $objectConfiguration->setProperties($properties); } } diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationParser.php b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationParser.php new file mode 100644 index 0000000000..ae38231f3f --- /dev/null +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationParser.php @@ -0,0 +1,259 @@ + $rawConfigurationOptions The configuration array with options for the object configuration + * @param string $configurationSourceHint A human readable hint on the original source of the configuration (for troubleshooting) + * @param Configuration|null $existingObjectConfiguration If set, this object configuration object will be used instead of creating a fresh one + * @return Configuration The object configuration object + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException + * @throws InvalidObjectConfigurationException if errors occurred during parsing + * @throws \ReflectionException + */ + public function parseConfigurationArray(string $objectName, array $rawConfigurationOptions, string $configurationSourceHint = '', Configuration|null $existingObjectConfiguration = null): Configuration + { + $className = $rawConfigurationOptions['className'] ?? $objectName; + $objectConfiguration = ($existingObjectConfiguration instanceof Configuration) ? $existingObjectConfiguration : new Configuration($objectName, $className); + $objectConfiguration->setConfigurationSourceHint($configurationSourceHint); + + foreach ($rawConfigurationOptions as $optionName => $optionValue) { + switch ($optionName) { + case 'scope': + $objectConfiguration->setScope(self::parseScope($optionValue)); + break; + case 'properties': + if (is_array($optionValue)) { + $objectConfiguration = $this->parsePropertyConfiguration($objectConfiguration, $optionValue); + } + break; + case 'arguments': + if (is_array($optionValue)) { + $objectConfiguration = $this->parseArgumentsConfiguration($objectConfiguration, $rawConfigurationOptions, $optionValue); + } + break; + case 'className': + $objectConfiguration->setClassName($rawConfigurationOptions['className']); + break; + case 'factoryObjectName': + $objectConfiguration->setFactoryObjectName(trim((string)$optionValue)); + break; + case 'factoryMethodName': + $objectConfiguration->setFactoryMethodName(trim((string)$optionValue)); + break; + case 'lifecycleInitializationMethodName': + $objectConfiguration->setLifecycleInitializationMethodName($optionValue); + break; + case 'lifecycleShutdownMethodName': + $objectConfiguration->setLifecycleShutdownMethodName($optionValue); + break; + case 'autowiring': + $objectConfiguration->setAutowiring(self::parseAutowiring($optionValue)); + break; + default: + throw new InvalidObjectConfigurationException('Invalid configuration option "' . $optionName . '" (source: ' . $objectConfiguration->getConfigurationSourceHint() . ')', 1167574981); + } + } + return $objectConfiguration; + } + + /** + * @param Configuration $objectConfiguration + * @param array $optionValue + * @return Configuration + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException + * @throws InvalidObjectConfigurationException + * @throws \ReflectionException + */ + protected function parsePropertyConfiguration(Configuration $objectConfiguration, array $optionValue): Configuration + { + foreach ($optionValue as $propertyName => $propertyValue) { + if (array_key_exists('value', $propertyValue)) { + $property = new ConfigurationProperty($propertyName, $propertyValue['value'], ConfigurationProperty::PROPERTY_TYPES_STRAIGHTVALUE); + } elseif (array_key_exists('object', $propertyValue)) { + $property = $this->parsePropertyOfTypeObject($propertyName, $propertyValue['object'], $objectConfiguration); + } elseif (array_key_exists('setting', $propertyValue)) { + $property = new ConfigurationProperty($propertyName, ['type' => ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'path' => $propertyValue['setting']], ConfigurationProperty::PROPERTY_TYPES_CONFIGURATION); + } else { + throw new InvalidObjectConfigurationException('Invalid configuration syntax. Expecting "value", "object" or "setting" as value for property "' . $propertyName . '", instead found "' . (is_array($propertyValue) ? implode(', ', array_keys($propertyValue)) : $propertyValue) . '" (source: ' . $objectConfiguration->getConfigurationSourceHint() . ')', 1230563249); + } + $objectConfiguration->setProperty($property); + } + + return $objectConfiguration; + } + + /** + * @param Configuration $objectConfiguration + * @param array $rawConfigurationOptions + * @param array $optionValue + * @return Configuration + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException + * @throws InvalidObjectConfigurationException + * @throws \ReflectionException + */ + protected function parseArgumentsConfiguration(Configuration $objectConfiguration, array $rawConfigurationOptions, array $optionValue): Configuration + { + foreach ($optionValue as $argumentName => $argumentValue) { + $argumentIndex = (int)$argumentName; + if (array_key_exists('value', $argumentValue)) { + $argument = new ConfigurationArgument($argumentIndex, $argumentValue['value'], ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE); + } elseif (array_key_exists('object', $argumentValue)) { + $argument = $this->parseArgumentOfTypeObject($argumentName, $argumentValue['object'], $objectConfiguration); + } elseif (array_key_exists('setting', $argumentValue)) { + $argument = new ConfigurationArgument($argumentIndex, $argumentValue['setting'], ConfigurationArgument::ARGUMENT_TYPES_SETTING); + } else { + throw new InvalidObjectConfigurationException('Invalid configuration syntax. Expecting "value", "object" or "setting" as value for argument "' . $argumentName . '", instead found "' . (is_array($argumentValue) ? implode(', ', array_keys($argumentValue)) : $argumentValue) . '" (source: ' . $objectConfiguration->getConfigurationSourceHint() . ')', 1230563250); + } + if (isset($rawConfigurationOptions['factoryObjectName']) || isset($rawConfigurationOptions['factoryMethodName'])) { + $objectConfiguration->setFactoryArgument($argument); + } else { + $objectConfiguration->setArgument($argument); + } + } + + return $objectConfiguration; + } + + /** + * Parses the configuration for arguments of type OBJECT + * + * @param string $argumentName Name of the argument + * @param mixed $objectNameOrConfiguration Value of the "object" section of the argument configuration - either a string or an array + * @param Configuration $parentObjectConfiguration The Configuration object this property belongs to + * @return ConfigurationArgument A configuration argument of type object + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException + * @throws InvalidObjectConfigurationException + * @throws \ReflectionException + */ + protected function parseArgumentOfTypeObject(string $argumentName, mixed $objectNameOrConfiguration, Configuration $parentObjectConfiguration): ConfigurationArgument + { + $objectName = null; + if (is_array($objectNameOrConfiguration)) { + if (isset($objectNameOrConfiguration['name'])) { + $objectName = $objectNameOrConfiguration['name']; + unset($objectNameOrConfiguration['name']); + } else { + $arguments = $this->reflectionService->getMethodParameters($parentObjectConfiguration->getClassName(), '__construct'); + if (is_numeric($argumentName)) { + foreach ($arguments as $argument) { + if ($argument['position'] === ((int)$argumentName - 1)) { + $objectName = $argument['type']; + } + } + } else { + $objectName = $arguments[$argumentName]['type']; + } + } + + if ($objectName === null) { + throw new InvalidObjectConfigurationException('Object configuration for argument "' . $argumentName . '" contains neither object name nor factory object or method name in ' . $parentObjectConfiguration->getConfigurationSourceHint(), 1417431742); + } + + $objectConfiguration = $this->parseConfigurationArray($objectName, $objectNameOrConfiguration, $parentObjectConfiguration->getConfigurationSourceHint() . ', argument "' . $argumentName . '"'); + $argument = new ConfigurationArgument($argumentName, $objectConfiguration, ConfigurationArgument::ARGUMENT_TYPES_OBJECT); + } else { + $argument = new ConfigurationArgument($argumentName, $objectNameOrConfiguration, ConfigurationArgument::ARGUMENT_TYPES_OBJECT); + } + return $argument; + } + + /** + * Parses the configuration for properties of type OBJECT + * + * @param string $propertyName Name of the property + * @param mixed $objectNameOrConfiguration Value of the "object" section of the property configuration - either a string or an array + * @param Configuration $parentObjectConfiguration The Configuration object this property belongs to + * @return ConfigurationProperty A configuration property of type object + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException + * @throws InvalidObjectConfigurationException + * @throws \ReflectionException + */ + protected function parsePropertyOfTypeObject(string $propertyName, mixed $objectNameOrConfiguration, Configuration $parentObjectConfiguration): ConfigurationProperty + { + if (is_array($objectNameOrConfiguration)) { + if (isset($objectNameOrConfiguration['name'])) { + $objectName = $objectNameOrConfiguration['name']; + unset($objectNameOrConfiguration['name']); + } else { + $propertyType = $this->reflectionService->getPropertyType($parentObjectConfiguration->getClassName(), $propertyName); + $objectName = $propertyType; + if ($objectName === null) { + $annotations = $this->reflectionService->getPropertyTagValues($parentObjectConfiguration->getClassName(), $propertyName, 'var'); + if (count($annotations) !== 1) { + throw new InvalidObjectConfigurationException(sprintf('Object %s (%s), for property "%s", contains neither object name, nor factory object name, and nor is the property properly @var - annotated.', $parentObjectConfiguration->getClassName(), $parentObjectConfiguration->getConfigurationSourceHint(), $propertyName), 1297097815); + } + $objectName = $annotations[0]; + } + } + $objectConfiguration = $this->parseConfigurationArray($objectName, $objectNameOrConfiguration, $parentObjectConfiguration->getConfigurationSourceHint() . ', property "' . $propertyName . '"'); + $property = new ConfigurationProperty($propertyName, $objectConfiguration, ConfigurationProperty::PROPERTY_TYPES_OBJECT); + } else { + $property = new ConfigurationProperty($propertyName, $objectNameOrConfiguration, ConfigurationProperty::PROPERTY_TYPES_OBJECT); + } + return $property; + } + + /** + * Parses the value of the option "scope" + * + * @param string $value Value of the option + * @return integer The scope translated into a Configuration::SCOPE_* constant + * @throws InvalidObjectConfigurationException if an invalid scope has been specified + */ + protected static function parseScope(string $value): int + { + switch ($value) { + case 'singleton': + return Configuration::SCOPE_SINGLETON; + case 'prototype': + return Configuration::SCOPE_PROTOTYPE; + case 'session': + return Configuration::SCOPE_SESSION; + default: + throw new InvalidObjectConfigurationException('Invalid scope "' . $value . '"', 1167574991); + } + } + + /** + * Parses the value of the option "autowiring" + * + * @param bool|int $value Value of the option + * @return integer The autowiring option translated into one of Configuration::AUTOWIRING_MODE_* + * @throws InvalidObjectConfigurationException if an invalid option has been specified + */ + protected static function parseAutowiring(bool|int $value): int + { + if ($value === true || $value === Configuration::AUTOWIRING_MODE_ON) { + return Configuration::AUTOWIRING_MODE_ON; + } + + if ($value === false || $value === Configuration::AUTOWIRING_MODE_OFF) { + return Configuration::AUTOWIRING_MODE_OFF; + } + + throw new InvalidObjectConfigurationException('Invalid autowiring declaration', 1283866757); + } +} diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationProperty.php b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationProperty.php index 75b2b19b60..42974419ee 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationProperty.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationProperty.php @@ -18,45 +18,12 @@ * * @Flow\Proxy(false) */ -class ConfigurationProperty +final readonly class ConfigurationProperty { - const PROPERTY_TYPES_STRAIGHTVALUE = 0; - const PROPERTY_TYPES_OBJECT = 1; - const PROPERTY_TYPES_CONFIGURATION = 2; - const PROPERTY_TYPES_CACHE = 3; - - /** - * @var string Name of the property - */ - protected $name; - - /** - * @var mixed Value of the property - */ - protected $value; - - /** - * @var integer Type of the property - one of the PROPERTY_TYPE_* constants - */ - protected $type; - - /** - * If specified, this configuration is used for instantiating / retrieving an property of type object - * @var Configuration - */ - protected $objectConfiguration = null; - - /** - * @var integer - */ - protected $autowiring = Configuration::AUTOWIRING_MODE_ON; - - /** - * Should this property be lazy loaded - * - * @var boolean - */ - protected $lazyLoading = true; + public const PROPERTY_TYPES_STRAIGHTVALUE = 0; + public const PROPERTY_TYPES_OBJECT = 1; + public const PROPERTY_TYPES_CONFIGURATION = 2; + public const PROPERTY_TYPES_CACHE = 3; /** * Constructor - sets the name, type and value of the property @@ -64,39 +31,25 @@ class ConfigurationProperty * @param string $name Name of the property * @param mixed $value Value of the property * @param integer $type Type of the property - one of the PROPERTY_TYPE_* constants - * @param Configuration $objectConfiguration If $type is OBJECT, a custom object configuration may be specified - * @param boolean $lazyLoading - */ - public function __construct($name, $value, $type = self::PROPERTY_TYPES_STRAIGHTVALUE, $objectConfiguration = null, $lazyLoading = true) - { - $this->set($name, $value, $type, $objectConfiguration, $lazyLoading); - } - - /** - * Sets the name, type and value of the property - * - * @param string $name Name of the property - * @param mixed $value Value of the property - * @param integer $type Type of the property - one of the PROPERTY_TYPE_* constants - * @param Configuration $objectConfiguration If $type is OBJECT, a custom object configuration may be specified - * @param boolean $lazyLoading - * @return void + * @param Configuration|null $objectConfiguration If $type is OBJECT, a custom object configuration may be specified + * @param boolean $lazyLoading Should this property be lazy loaded + * @param int $autowiring */ - public function set($name, $value, $type = self::PROPERTY_TYPES_STRAIGHTVALUE, $objectConfiguration = null, $lazyLoading = true) - { - $this->name = $name; - $this->value = $value; - $this->type = $type; - $this->objectConfiguration = $objectConfiguration; - $this->lazyLoading = $lazyLoading; - } + public function __construct( + private string $name, + private mixed $value, + private int $type = self::PROPERTY_TYPES_STRAIGHTVALUE, + private Configuration|null $objectConfiguration = null, + private bool $lazyLoading = true, + private int $autowiring = Configuration::AUTOWIRING_MODE_ON + ) {} /** * Returns the name of the property * * @return string Name of the property */ - public function getName() + public function getName(): string { return $this->name; } @@ -106,7 +59,7 @@ public function getName() * * @return mixed Value of the property */ - public function getValue() + public function getValue(): mixed { return $this->value; } @@ -116,7 +69,7 @@ public function getValue() * * @return integer Type of the property */ - public function getType() + public function getType(): int { return $this->type; } @@ -126,28 +79,17 @@ public function getType() * * @return Configuration|null The object configuration or NULL */ - public function getObjectConfiguration() + public function getObjectConfiguration(): Configuration|null { return $this->objectConfiguration; } - /** - * Sets autowiring for this property - * - * @param integer $autowiring One of the Configuration::AUTOWIRING_MODE_* constants - * @return void - */ - public function setAutowiring($autowiring) - { - $this->autowiring = $autowiring; - } - /** * Returns the autowiring mode for this property * * @return integer Value of one of the Configuration::AUTOWIRING_MODE_* constants */ - public function getAutowiring() + public function getAutowiring(): int { return $this->autowiring; } @@ -157,7 +99,7 @@ public function getAutowiring() * * @return boolean */ - public function isLazyLoading() + public function isLazyLoading(): bool { return $this->lazyLoading; } diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php index 6c219ba83b..9668022b75 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php @@ -18,43 +18,33 @@ * * @Flow\Proxy(false) * @api + * @template T of object */ -class DependencyProxy +final class DependencyProxy { - /** - * @var string - */ - protected $className; - - /** - * @var \Closure - */ - protected $builder; - /** * @var array */ - protected $propertyVariables = []; + protected array $propertyVariables = []; /** * Constructs this proxy * - * @param string $className Implementation class name of the dependency to proxy + * @param class-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; - } + public function __construct( + protected string $className, + protected \Closure $builder + ) {} /** * Activate the dependency and set it in the object. * - * @return object The real dependency object + * @return T The real dependency object * @api */ - public function _activateDependency() + public function _activateDependency(): object { $realDependency = $this->builder->__invoke(); foreach ($this->propertyVariables as &$propertyVariable) { @@ -66,10 +56,10 @@ public function _activateDependency() /** * Returns the class name of the proxied dependency * - * @return string Fully qualified class name of the proxied object + * @return class-string Fully qualified class name of the proxied object * @api */ - public function _getClassName() + public function _getClassName(): string { return $this->className; } @@ -81,7 +71,7 @@ public function _getClassName() * @param mixed &$propertyVariable The variable to replace * @return void */ - public function _addPropertyVariable(&$propertyVariable) + public function _addPropertyVariable(&$propertyVariable): void { $this->propertyVariables[] = &$propertyVariable; } @@ -94,7 +84,7 @@ public function _addPropertyVariable(&$propertyVariable) * @param array $arguments An array of arguments to be passed to the method * @return mixed */ - public function __call($methodName, array $arguments) + public function __call(string $methodName, array $arguments): mixed { return $this->_activateDependency()->$methodName(...$arguments); } diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/PropertyInjectionTrait.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/PropertyInjectionTrait.php index 36e0663fc6..8327115dce 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/PropertyInjectionTrait.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/PropertyInjectionTrait.php @@ -22,14 +22,14 @@ trait PropertyInjectionTrait * Does a property injection lazily with fallbacks. * Used in proxy classes. * - * @param string $propertyObjectName + * @param class-string $propertyObjectName * @param string $propertyClassName * @param string $propertyName * @param string $setterArgumentHash * @param callable $lazyInjectionResolver * @return void */ - private function Flow_Proxy_LazyPropertyInjection($propertyObjectName, $propertyClassName, $propertyName, $setterArgumentHash, callable $lazyInjectionResolver) + private function Flow_Proxy_LazyPropertyInjection(string $propertyObjectName, string $propertyClassName, string $propertyName, string $setterArgumentHash, callable $lazyInjectionResolver): void { $injection_reference = &$this->$propertyName; $this->$propertyName = \Neos\Flow\Core\Bootstrap::$staticObjectManager->getInstance($propertyObjectName); diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php index 62165d5fcf..c40e7a2ef6 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php @@ -12,25 +12,29 @@ */ 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; +use Neos\Flow\Core\Bootstrap; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\ObjectManagement\CompileTimeObjectManager; use Neos\Flow\ObjectManagement\Configuration\Configuration; use Neos\Flow\ObjectManagement\Configuration\ConfigurationArgument; use Neos\Flow\ObjectManagement\Configuration\ConfigurationProperty; use Neos\Flow\ObjectManagement\Exception as ObjectException; -use Neos\Flow\ObjectManagement\Exception\InvalidObjectConfigurationException; +use Neos\Flow\ObjectManagement\Exception\CannotBuildObjectException; use Neos\Flow\ObjectManagement\Exception\UnknownObjectException; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\ObjectManagement\Proxy\Compiler; use Neos\Flow\ObjectManagement\Proxy\ObjectSerializationTrait; use Neos\Flow\ObjectManagement\Proxy\ProxyClass; use Neos\Flow\ObjectManagement\Proxy\ProxyMethodGenerator; +use Neos\Flow\Reflection\Exception\ClassLoadingForReflectionFailedException; +use Neos\Flow\Reflection\Exception\InvalidClassException; use Neos\Flow\Reflection\MethodReflection; use Neos\Flow\Reflection\ReflectionService; -use Neos\Utility\Arrays; +use Neos\Utility\TypeHandling; use Psr\Log\LoggerInterface; /** @@ -42,56 +46,29 @@ class ProxyClassBuilder { public const AUTOGENERATED_PROXY_METHOD_COMMENT = 'Autogenerated Proxy Method'; - protected ReflectionService $reflectionService; - protected Compiler $compiler; - protected LoggerInterface $logger; - protected ConfigurationManager $configurationManager; - protected CacheManager $cacheManager; - protected CompileTimeObjectManager $objectManager; - /** * @var array */ protected array $objectConfigurations = []; - public function injectReflectionService(ReflectionService $reflectionService): void - { - $this->reflectionService = $reflectionService; - } - - public function injectCompiler(Compiler $compiler): void - { - $this->compiler = $compiler; - } - - public function injectConfigurationManager(ConfigurationManager $configurationManager): void - { - $this->configurationManager = $configurationManager; - } - - public function injectCacheManager(CacheManager $cacheManager): void - { - $this->cacheManager = $cacheManager; - } - - #[Flow\Autowiring(false)] - public function injectLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - } - - public function injectObjectManager(CompileTimeObjectManager $objectManager): void - { - $this->objectManager = $objectManager; - } + public function __construct( + protected ReflectionService $reflectionService, + protected Compiler $compiler, + protected LoggerInterface $logger, + protected ConfigurationManager $configurationManager, + protected CacheManager $cacheManager, + protected CompileTimeObjectManager $objectManager, + ) {} /** * Analyzes the object configuration provided by the compile-time object manager and builds * the necessary PHP code for the proxy classes to realize dependency injection. * * @throws ObjectException + * @throws CannotBuildObjectException * @throws UnknownObjectException - * @throws InvalidConfigurationTypeException + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException * @throws \ReflectionException */ public function build(): void @@ -111,36 +88,25 @@ public function build(): void if ($proxyClass === false) { continue; } - $this->logger->debug(sprintf('Building dependency injection proxy for "%s"', $className), LogEnvironment::fromMethodName(__METHOD__)); + $this->logger->debug(sprintf('Building dependency injection proxyG:q for "%s"', $className), LogEnvironment::fromMethodName(__METHOD__)); - $constructor = $proxyClass->getConstructor(); - $constructor->addPreParentCallCode($this->buildSetInstanceCode($objectConfiguration)); - $constructor->addPreParentCallCode($this->buildConstructorInjectionCode($objectConfiguration)); + $injectionCodeWasIntroduced = false; - $sleepMethod = $proxyClass->getMethod('__sleep'); - $sleepMethod->setDocBlock(self::AUTOGENERATED_PROXY_METHOD_COMMENT); - $sleepMethod->setReturnType('array'); + $constructor = $proxyClass->getConstructor(); $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->setReturnType('void'); $wakeupMethod->addPostParentCallCode($this->buildLifecycleInitializationCode($objectConfiguration, ObjectManagerInterface::INITIALIZATIONCAUSE_RECREATED)); $wakeupMethod->addPostParentCallCode($this->buildLifecycleShutdownCode($objectConfiguration, ObjectManagerInterface::INITIALIZATIONCAUSE_RECREATED)); $injectPropertiesCode = $this->buildPropertyInjectionCode($objectConfiguration); - if ($injectPropertiesCode !== '') { + if ($injectPropertiesCode !== []) { + $injectionCodeWasIntroduced = true; $proxyClass->addTraits(['\\' . PropertyInjectionTrait::class]); $injectPropertiesMethod = $proxyClass->getMethod('Flow_Proxy_injectProperties'); - $injectPropertiesMethod->addPreParentCallCode($injectPropertiesCode); + $injectPropertiesMethod->addPreParentCallCode(implode(PHP_EOL, $injectPropertiesCode)); $injectPropertiesMethod->setVisibility(ProxyMethodGenerator::VISIBILITY_PRIVATE); $injectPropertiesMethod->setDocBlock(self::AUTOGENERATED_PROXY_METHOD_COMMENT); @@ -151,13 +117,32 @@ public function build(): void ); $constructor->addPostParentCallCode( <<Flow_Proxy_injectProperties(); } PHP ); } + $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)); @@ -167,30 +152,6 @@ public function build(): void } } - /** - * Renders additional code which registers the instance of the proxy class at the Object Manager - * before constructor injection is executed. Used in constructors and wakeup methods. - * - * This also makes sure that object creation does not end in an endless loop due to bidirectional dependencies. - */ - protected function buildSetInstanceCode(Configuration $objectConfiguration): string - { - if ($objectConfiguration->getScope() === Configuration::SCOPE_PROTOTYPE) { - return ''; - } - - $code = 'if (get_class($this) === \'' . $objectConfiguration->getClassName() . '\') \Neos\Flow\Core\Bootstrap::$staticObjectManager->setInstance(\'' . $objectConfiguration->getObjectName() . '\', $this);'; - - $className = $objectConfiguration->getClassName(); - foreach ($this->objectConfigurations as $otherObjectConfiguration) { - if ($otherObjectConfiguration !== $objectConfiguration && $otherObjectConfiguration->getClassName() === $className) { - $code .= 'if (get_class($this) === \'' . $otherObjectConfiguration->getClassName() . '\') \Neos\Flow\Core\Bootstrap::$staticObjectManager->setInstance(\'' . $otherObjectConfiguration->getObjectName() . '\', $this);'; - } - } - - return $code; - } - /** * Renders code to create identifier/type information from related entities in an object. * @@ -208,14 +169,12 @@ 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 { + /** @var class-string $className */ $className = $objectConfiguration->getClassName(); - - if ($this->reflectionService->hasMethod($className, '__sleep')) { - return ''; - } - + $forceSerializationCode = $forceSerializationCode === false ? ($this->reflectionService->getClassAnnotation($className, Flow\Proxy::class)?->forceSerializationCode ?? false) : true; + /** @var Flow\Scope $scopeAnnotation */ $scopeAnnotation = $this->reflectionService->getClassAnnotation($className, Flow\Scope::class); $transientProperties = $this->reflectionService->getPropertyNamesByAnnotation($className, Flow\Transient::class); $injectedProperties = $this->reflectionService->getPropertyNamesByAnnotation($className, Flow\Inject::class); @@ -223,9 +182,9 @@ protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfig $doBuildCode = $this->reflectionService->getClassAnnotation($className, Flow\Entity::class) !== null; $doBuildCode = $doBuildCode || (count($transientProperties) > 0); $doBuildCode = $doBuildCode || (count($injectedProperties) > 0); - $doBuildCode = $doBuildCode || ($scopeAnnotation->value ?? 'prototype') === 'session'; + $doBuildCode = $doBuildCode || ($scopeAnnotation && $scopeAnnotation->value === 'session'); - if ($doBuildCode === false) { + if (!$forceSerializationCode && $doBuildCode === false) { return ''; } @@ -235,7 +194,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 +205,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,115 +212,16 @@ protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfig protected function buildSetRelatedEntitiesCode(): string { - return "\n " . '$this->Flow_setRelatedEntities();' . "\n"; - } - - /** - * Renders additional code for the __construct() method of the Proxy Class which realizes constructor injection. - * - * @throws InvalidConfigurationTypeException - * @throws UnknownObjectException - * @throws InvalidObjectConfigurationException - */ - protected function buildConstructorInjectionCode(Configuration $objectConfiguration): string - { - $doReturnCode = $objectConfiguration->getScope() !== Configuration::SCOPE_PROTOTYPE; - - $assignments = []; - $argumentConfigurations = $objectConfiguration->getArguments(); - $constructorParameterInfo = $this->reflectionService->getMethodParameters($objectConfiguration->getClassName(), '__construct'); - $argumentNumberToOptionalInfo = []; - - foreach ($constructorParameterInfo as $parameterInfo) { - $argumentNumberToOptionalInfo[($parameterInfo['position'] + 1)] = $parameterInfo['optional']; - } - - $highestArgumentPositionWithAutowiringEnabled = -1; - foreach ($argumentConfigurations as $argumentNumber => $argumentConfiguration) { - if (!$argumentConfiguration instanceof ConfigurationArgument) { - continue; - } - $argumentPosition = $argumentNumber - 1; - if ($argumentConfiguration->getAutowiring() === Configuration::AUTOWIRING_MODE_ON) { - $highestArgumentPositionWithAutowiringEnabled = $argumentPosition; - } - - $argumentValue = $argumentConfiguration->getValue(); - $assignmentPrologue = 'if (!array_key_exists(' . ($argumentNumber - 1) . ', $arguments)) $arguments[' . ($argumentNumber - 1) . '] = '; - if ($argumentValue === null && isset($argumentNumberToOptionalInfo[$argumentNumber]) && $argumentNumberToOptionalInfo[$argumentNumber] === true) { - $assignments[$argumentPosition] = $assignmentPrologue . 'NULL'; - } else { - switch ($argumentConfiguration->getType()) { - case ConfigurationArgument::ARGUMENT_TYPES_OBJECT: - if ($argumentValue instanceof Configuration) { - $doReturnCode = true; - $argumentValueObjectName = $argumentValue->getObjectName(); - $argumentValueClassName = $argumentValue->getClassName(); - if ($argumentValueClassName === null) { - $preparedArgument = $this->buildCustomFactoryCall($argumentValue->getFactoryObjectName(), $argumentValue->getFactoryMethodName(), $argumentValue->getFactoryArguments()); - $assignments[$argumentPosition] = $assignmentPrologue . $preparedArgument; - } elseif ($this->objectConfigurations[$argumentValueObjectName]->getScope() === Configuration::SCOPE_PROTOTYPE) { - $assignments[$argumentPosition] = $assignmentPrologue . 'new \\' . $argumentValueObjectName . '(' . $this->buildMethodParametersCode($argumentValue->getArguments()) . ')'; - } else { - $assignments[$argumentPosition] = $assignmentPrologue . '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $argumentValueObjectName . '\')'; - } - } else { - if (str_contains($argumentValue, '.')) { - $settingPath = explode('.', $argumentValue); - $settings = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, array_shift($settingPath)); - $argumentValue = Arrays::getValueByPath($settings, $settingPath); - } - if (!isset($this->objectConfigurations[$argumentValue])) { - throw new UnknownObjectException('The object "' . $argumentValue . '" which was specified as an argument in the object configuration of object "' . $objectConfiguration->getObjectName() . '" does not exist.', 1264669967); - } - $assignments[$argumentPosition] = $assignmentPrologue . '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $argumentValue . '\')'; - $doReturnCode = true; - } - break; - - case ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE: - $assignments[$argumentPosition] = $assignmentPrologue . var_export($argumentValue, true); - if ($argumentValue !== null) { - $doReturnCode = true; - } - 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 . '\')'; - $doReturnCode = true; - break; - } - } - } - - for ($argumentCounter = count($assignments) - 1; $argumentCounter > $highestArgumentPositionWithAutowiringEnabled; $argumentCounter--) { - unset($assignments[$argumentCounter]); - } - - $code = $argumentCounter >= 0 ? "\n" . implode(";\n", $assignments) . ";\n" : ''; - - $index = 0; - foreach ($constructorParameterInfo as $parameterName => $parameterInfo) { - if ($parameterInfo['optional'] === true) { - break; - } - if ($objectConfiguration->getScope() === Configuration::SCOPE_SINGLETON) { - $code .= 'if (!array_key_exists(' . $index . ', $arguments)) throw new \Neos\Flow\ObjectManagement\Exception\UnresolvedDependenciesException(\'Missing required constructor argument $' . $parameterName . ' in class \' . __CLASS__ . \'. Please check your calling code and Dependency Injection configuration.\', 1296143787);' . "\n"; - } else { - $code .= 'if (!array_key_exists(' . $index . ', $arguments)) throw new \Neos\Flow\ObjectManagement\Exception\UnresolvedDependenciesException(\'Missing required constructor argument $' . $parameterName . ' in class \' . __CLASS__ . \'. Note that constructor injection is only support for objects of scope singleton (and this is not a singleton) – for other scopes you must pass each required argument to the constructor yourself.\', 1296143788);' . "\n"; - } - $index++; - } - - return $doReturnCode ? $code : ''; + return PHP_EOL . '$this->Flow_setRelatedEntities();' . PHP_EOL; } /** * Builds the code necessary to inject setter based dependencies. * + * @return string[] php code * @throws UnknownObjectException */ - protected function buildPropertyInjectionCode(Configuration $objectConfiguration): string + protected function buildPropertyInjectionCode(Configuration $objectConfiguration): array { $commands = []; $injectedProperties = []; @@ -384,17 +242,7 @@ protected function buildPropertyInjectionCode(Configuration $objectConfiguration break; case ConfigurationProperty::PROPERTY_TYPES_STRAIGHTVALUE: - if (is_string($propertyValue)) { - $preparedSetterArgument = '\'' . str_replace('\'', '\\\'', $propertyValue) . '\''; - } elseif (is_array($propertyValue)) { - $preparedSetterArgument = var_export($propertyValue, true); - } elseif (is_bool($propertyValue)) { - $preparedSetterArgument = $propertyValue ? 'true' : 'false'; - } elseif ($propertyValue === null) { - $preparedSetterArgument = 'null'; - } else { - $preparedSetterArgument = $propertyValue; - } + $preparedSetterArgument = var_export($propertyValue, true); $commands[] = 'if (\Neos\Utility\ObjectAccess::setProperty($this, \'' . $propertyName . '\', ' . $preparedSetterArgument . ') === false) { $this->' . $propertyName . ' = ' . $preparedSetterArgument . ';}'; break; case ConfigurationProperty::PROPERTY_TYPES_CONFIGURATION: @@ -416,13 +264,10 @@ protected function buildPropertyInjectionCode(Configuration $objectConfiguration } if (count($commands) > 0) { - $commandString = implode(PHP_EOL, $commands) . PHP_EOL; - $commandString .= '$this->Flow_Injected_Properties = ' . var_export($injectedProperties, true) . ";\n"; - } else { - $commandString = ''; + $commands[] = '$this->Flow_Injected_Properties = ' . var_export($injectedProperties, true) . ";" . PHP_EOL; } - return $commandString; + return $commands; } /** @@ -431,7 +276,7 @@ protected function buildPropertyInjectionCode(Configuration $objectConfiguration * @param Configuration $objectConfiguration Configuration of the object to inject into * @param string $propertyName Name of the property to inject * @param Configuration $propertyConfiguration Configuration of the object to inject - * @return array lines of PHP code + * @return string[] lines of PHP code * @throws UnknownObjectException */ protected function buildPropertyInjectionCodeByConfiguration(Configuration $objectConfiguration, $propertyName, Configuration $propertyConfiguration): array @@ -439,17 +284,17 @@ protected function buildPropertyInjectionCodeByConfiguration(Configuration $obje $className = $objectConfiguration->getClassName(); $propertyObjectName = $propertyConfiguration->getObjectName(); $propertyClassName = $propertyConfiguration->getClassName(); - if ($propertyClassName === null) { + if ($propertyConfiguration->getFactoryObjectName()) { $preparedSetterArgument = $this->buildCustomFactoryCall($propertyConfiguration->getFactoryObjectName(), $propertyConfiguration->getFactoryMethodName(), $propertyConfiguration->getFactoryArguments()); } else { - if (!is_string($propertyClassName) || !isset($this->objectConfigurations[$propertyClassName])) { + if (!isset($this->objectConfigurations[$propertyClassName])) { $configurationSource = $objectConfiguration->getConfigurationSourceHint(); throw new UnknownObjectException('Unknown class "' . $propertyClassName . '", specified as property "' . $propertyName . '" in the object configuration of object "' . $objectConfiguration->getObjectName() . '" (' . $configurationSource . ').', 1296130876); } if ($this->objectConfigurations[$propertyClassName]->getScope() === Configuration::SCOPE_PROTOTYPE) { $preparedSetterArgument = 'new \\' . $propertyClassName . '(' . $this->buildMethodParametersCode($propertyConfiguration->getArguments()) . ')'; } else { - $preparedSetterArgument = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $propertyClassName . '\')'; + $preparedSetterArgument = $this->buildStaticObjectManagerCode($propertyClassName); } } @@ -468,10 +313,11 @@ protected function buildPropertyInjectionCodeByConfiguration(Configuration $obje * @param ConfigurationProperty $propertyConfiguration * @param string $propertyName Name of the property to inject * @param string $propertyObjectName Object name of the object to inject - * @return array lines of PHP code + * @return string[] lines of PHP code * @throws UnknownObjectException + * @throws InvalidConfigurationTypeException */ - public function buildPropertyInjectionCodeByString(Configuration $objectConfiguration, ConfigurationProperty $propertyConfiguration, $propertyName, $propertyObjectName): array + public function buildPropertyInjectionCodeByString(Configuration $objectConfiguration, ConfigurationProperty $propertyConfiguration, string $propertyName, string $propertyObjectName): array { $className = $objectConfiguration->getClassName(); if (!isset($this->objectConfigurations[$propertyObjectName])) { @@ -488,7 +334,7 @@ public function buildPropertyInjectionCodeByString(Configuration $objectConfigur if ($this->objectConfigurations[$propertyObjectName]->getScope() === Configuration::SCOPE_PROTOTYPE && !$this->objectConfigurations[$propertyObjectName]->isCreatedByFactory()) { $preparedSetterArgument = 'new \\' . $propertyClassName . '(' . $this->buildMethodParametersCode($this->objectConfigurations[$propertyObjectName]->getArguments()) . ')'; } else { - $preparedSetterArgument = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $propertyObjectName . '\')'; + $preparedSetterArgument = $this->buildStaticObjectManagerCode($propertyObjectName); } $result = $this->buildSetterInjectionCode($className, $propertyName, $preparedSetterArgument); @@ -511,17 +357,16 @@ public function buildPropertyInjectionCodeByString(Configuration $objectConfigur * @param string $propertyName Name of the property to inject * @param string $configurationType the configuration type of the injected property (one of the ConfigurationManager::CONFIGURATION_TYPE_* constants) * @param string|null $configurationPath Path with "." as separator specifying the setting value to inject or NULL if the complete configuration array should be injected - * @return array PHP code + * @return string[] PHP code */ - public function buildPropertyInjectionCodeByConfigurationTypeAndPath(Configuration $objectConfiguration, $propertyName, $configurationType, $configurationPath = null): array + public function buildPropertyInjectionCodeByConfigurationTypeAndPath(Configuration $objectConfiguration, string $propertyName, string $configurationType, ?string $configurationPath = null): array { $className = $objectConfiguration->getClassName(); + $preparedSetterArgument = $this->buildStaticObjectManagerCode(ConfigurationManager::class) . '->getConfiguration(\'' . $configurationType . '\''; if ($configurationPath !== null) { - $preparedSetterArgument = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\Flow\Configuration\ConfigurationManager::class)->getConfiguration(\'' . $configurationType . '\', \'' . $configurationPath . '\')'; - } else { - $preparedSetterArgument = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\Flow\Configuration\ConfigurationManager::class)->getConfiguration(\'' . $configurationType . '\')'; + $preparedSetterArgument .= ', \'' . $configurationPath . '\''; } - + $preparedSetterArgument .= ')'; $result = $this->buildSetterInjectionCode($className, $propertyName, $preparedSetterArgument); if ($result !== null) { return $result; @@ -535,12 +380,12 @@ public function buildPropertyInjectionCodeByConfigurationTypeAndPath(Configurati * @param Configuration $objectConfiguration Configuration of the object to inject into * @param string $propertyName Name of the property to inject * @param string $cacheIdentifier the identifier of the cache to inject - * @return array PHP code + * @return string[] PHP code */ public function buildPropertyInjectionCodeByCacheIdentifier(Configuration $objectConfiguration, string $propertyName, string $cacheIdentifier): array { $className = $objectConfiguration->getClassName(); - $preparedSetterArgument = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\Flow\Cache\CacheManager::class)->getCache(\'' . $cacheIdentifier . '\')'; + $preparedSetterArgument = $this->buildStaticObjectManagerCode(CacheManager::class) . '->getCache(\'' . $cacheIdentifier . '\')'; $result = $this->buildSetterInjectionCode($className, $propertyName, $preparedSetterArgument); if ($result !== null) { return $result; @@ -555,9 +400,9 @@ public function buildPropertyInjectionCodeByCacheIdentifier(Configuration $objec * @param string $propertyClassName Class name of the dependency to inject * @param string $propertyName Name of the property in the class to inject into * @param string $preparedSetterArgument PHP code to use for retrieving the value to inject - * @return array PHP code + * @return string[] PHP code */ - protected function buildLazyPropertyInjectionCode($propertyObjectName, $propertyClassName, $propertyName, $preparedSetterArgument): array + protected function buildLazyPropertyInjectionCode(string $propertyObjectName, string $propertyClassName, string $propertyName, string $preparedSetterArgument): array { $setterArgumentHash = "'" . md5($preparedSetterArgument) . "'"; $commands[] = '$this->Flow_Proxy_LazyPropertyInjection(\'' . $propertyObjectName . '\', \'' . $propertyClassName . '\', \'' . $propertyName . '\', ' . $setterArgumentHash . ', function() { return ' . $preparedSetterArgument . '; });'; @@ -572,7 +417,7 @@ protected function buildLazyPropertyInjectionCode($propertyObjectName, $property * * If neither inject*() nor set*() exists, but the property does exist, NULL is returned * - * @param string $className Name of the class to inject into + * @param class-string $className Name of the class to inject into * @param string $propertyName Name of the property to inject * @param string $preparedSetterArgument PHP code to use for retrieving the value to inject * @@ -609,19 +454,9 @@ protected function buildLifecycleInitializationCode(Configuration $objectConfigu if (!$this->reflectionService->hasMethod($objectConfiguration->getClassName(), $lifecycleInitializationMethodName)) { return ''; } - $className = $objectConfiguration->getClassName(); - $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"; - } else { - $code .= "\n" . ' if ($isSameClass) {' . "\n"; - } - $code .= ' $this->' . $lifecycleInitializationMethodName . '(' . $cause . ');' . "\n"; - $code .= ' }' . "\n"; - return $code; + + $lifeCycleCode = ' $this->' . $lifecycleInitializationMethodName . '(' . $cause . ');'; + return implode(PHP_EOL, $this->wrapLifeCycleChecksCode($objectConfiguration->getClassName(), $lifeCycleCode, $cause)); } /** @@ -637,20 +472,32 @@ protected function buildLifecycleShutdownCode(Configuration $objectConfiguration if (!$this->reflectionService->hasMethod($objectConfiguration->getClassName(), $lifecycleShutdownMethodName)) { return ''; } - $className = $objectConfiguration->getClassName(); - $code = '$isSameClass = get_class($this) === \'' . $className . '\';'; + + $lifeCycleCode = ' \Neos\Flow\Core\Bootstrap::$staticObjectManager->registerShutdownObject($this, \'' . $lifecycleShutdownMethodName . '\');'; + return implode(PHP_EOL, $this->wrapLifeCycleChecksCode($objectConfiguration->getClassName(), $lifeCycleCode, $cause)); + } + + /** + * @param string $className + * @param string $lifeCycleCode + * @param int $cause + * @return string[] + */ + protected function wrapLifeCycleChecksCode(string $className, string $lifeCycleCode, int $cause): array + { + $codeLines = []; + $codeLines[] = '$isSameClass = get_class($this) === \\' . $className . '::class;'; 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"; + $codeLines[] = '$classParents = class_parents($this);'; + $codeLines[] = '$classImplements = class_implements($this);'; + $codeLines[] = '$isClassProxy = array_search(\\' . $className . '::class, $classParents) !== false && array_search(\\Doctrine\Persistence\Proxy::class, $classImplements) !== false;'; + $codeLines[] = 'if ($isSameClass || $isClassProxy) {'; } else { - $code .= "\n" . 'if ($isSameClass) {' . "\n"; + $codeLines[] = 'if ($isSameClass) {'; } - $code .= ' \Neos\Flow\Core\Bootstrap::$staticObjectManager->registerShutdownObject($this, \'' . $lifecycleShutdownMethodName . '\');' . PHP_EOL; - $code .= '}' . "\n"; - - return $code; + $codeLines[] = $lifeCycleCode; + $codeLines[] = '}'; + return $codeLines; } /** @@ -677,13 +524,13 @@ protected function buildMethodParametersCode(array $argumentConfigurations): str if ($this->objectConfigurations[$argumentValueObjectName]->getScope() === Configuration::SCOPE_PROTOTYPE) { $preparedArguments[] = 'new \\' . $argumentValueObjectName . '(' . $this->buildMethodParametersCode($argumentValue->getArguments()) . ')'; } else { - $preparedArguments[] = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $argumentValueObjectName . '\')'; + $preparedArguments[] = $this->buildStaticObjectManagerCode($argumentValueObjectName); } } else { if (str_contains($argumentValue, '.')) { $argumentValue = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, $argumentValue); } - $preparedArguments[] = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $argumentValue . '\')'; + $preparedArguments[] = $this->buildStaticObjectManagerCode($argumentValue); } break; @@ -692,7 +539,7 @@ protected function buildMethodParametersCode(array $argumentConfigurations): str 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 . '\')'; + $preparedArguments[] = $this->buildStaticObjectManagerCode(ConfigurationManager::class) . '->getConfiguration(\Neos\Flow\Configuration\ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, \'' . $argumentValue . '\')'; break; } } @@ -701,21 +548,21 @@ protected function buildMethodParametersCode(array $argumentConfigurations): str } /** - * @param string $customFactoryObjectName + * @param class-string $customFactoryObjectName * @param string $customFactoryMethodName * @param array $arguments * @return string */ - protected function buildCustomFactoryCall($customFactoryObjectName, $customFactoryMethodName, array $arguments): string + protected function buildCustomFactoryCall(string $customFactoryObjectName, string $customFactoryMethodName, array $arguments): string { $parametersCode = $this->buildMethodParametersCode($arguments); - return ($customFactoryObjectName ? '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $customFactoryObjectName . '\')->' : '') . $customFactoryMethodName . '(' . $parametersCode . ')'; + return ($customFactoryObjectName ? $this->buildStaticObjectManagerCode($customFactoryObjectName) . '->' : '') . $customFactoryMethodName . '(' . $parametersCode . ')'; } /** * Compile the result of methods marked with CompileStatic into the proxy class * - * @param string $className + * @param class-string $className * @param ProxyClass $proxyClass * @return void * @throws ObjectException @@ -739,4 +586,74 @@ protected function compileStaticMethods(string $className, ProxyClass $proxyClas $compiledMethod->setBody('return ' . $compiledResult . ';'); } } + + protected function couldHaveEntityRelations(Configuration $objectConfiguration): bool + { + $result = false; + /** @var class-string $className */ + $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; + } + + protected function buildStaticObjectManagerCode(string $objectName): string + { + // we could do something like "$objectName . '::class'" here, but + // it might reference a virtual object, so this neds to stay a string. + return '\\' . Bootstrap::class . '::$staticObjectManager->get(\'' . $objectName . '\')'; + } } diff --git a/Neos.Flow/Classes/ObjectManagement/ObjectManager.php b/Neos.Flow/Classes/ObjectManagement/ObjectManager.php index 6a03f2c458..fe098bc008 100644 --- a/Neos.Flow/Classes/ObjectManagement/ObjectManager.php +++ b/Neos.Flow/Classes/ObjectManagement/ObjectManager.php @@ -16,6 +16,7 @@ use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException; use Neos\Flow\ObjectManagement\Configuration\Configuration as ObjectConfiguration; +use Neos\Flow\ObjectManagement\Configuration\ConfigurationArgument; use Neos\Flow\ObjectManagement\Configuration\ConfigurationArgument as ObjectConfigurationArgument; use Neos\Flow\Core\ApplicationContext; use Neos\Flow\Annotations as Flow; @@ -34,6 +35,9 @@ class ObjectManager implements ObjectManagerInterface protected const KEY_SCOPE = 's'; protected const KEY_FACTORY = 'f'; protected const KEY_FACTORY_ARGUMENTS = 'fa'; + + protected const KEY_CONSTRUCTOR_ARGUMENTS = 'ca'; + protected const KEY_ARGUMENT_TYPE = 't'; protected const KEY_ARGUMENT_VALUE = 'v'; protected const KEY_CLASS_NAME = 'c'; @@ -48,14 +52,7 @@ class ObjectManager implements ObjectManagerInterface protected ApplicationContext $context; /** - * An array of settings of all packages, indexed by package key - * - * @var array - */ - protected array $allSettings = []; - - /** - * @var array + * @var array> */ protected array $objects = []; @@ -65,12 +62,12 @@ class ObjectManager implements ObjectManagerInterface protected array $dependencyProxies = []; /** - * @var array + * @var array */ protected array $classesBeingInstantiated = []; /** - * @var array + * @var array */ protected array $cachedLowerCasedObjectNames = []; @@ -105,7 +102,7 @@ public function __construct(ApplicationContext $context) /** * Sets the objects array * - * @param array $objects An array of object names and some information about each registered object (scope, lower cased name etc.) + * @param array $objects An array of object names and some information about each registered object (scope, lower cased name etc.) * @return void */ public function setObjects(array $objects): void @@ -115,18 +112,6 @@ public function setObjects(array $objects): void $this->objects[get_class($this)][self::KEY_INSTANCE] = $this; } - /** - * Injects the global settings array, indexed by package key. - * - * @param array $settings The global settings - * @return void - * @Flow\Autowiring(false) - */ - public function injectAllSettings(array $settings): void - { - $this->allSettings = $settings; - } - /** * Returns the context Flow is running in. * @@ -225,11 +210,21 @@ public function get($objectName, ...$constructorArguments): object throw new Exception\UnknownObjectException('Object "' . $objectName . '" is not registered.' . $hint, 1264589155); } + // by object name + if (isset($this->objects[$objectName][self::KEY_CONSTRUCTOR_ARGUMENTS])) { + $constructorArguments = $this->autowireArguments($this->objects[$objectName][self::KEY_CONSTRUCTOR_ARGUMENTS], $constructorArguments); + } + + // by class name + if ($objectName !== $className && isset($this->objects[$className][self::KEY_CONSTRUCTOR_ARGUMENTS])) { + $constructorArguments = $this->autowireArguments($this->objects[$className][self::KEY_CONSTRUCTOR_ARGUMENTS], $constructorArguments); + } + if (!isset($this->objects[$objectName]) || $this->objects[$objectName][self::KEY_SCOPE] === ObjectConfiguration::SCOPE_PROTOTYPE) { return $this->instantiateClass($className, $constructorArguments); } - $this->objects[$objectName][self::KEY_INSTANCE] = $this->instantiateClass($className, []); + $this->objects[$objectName][self::KEY_INSTANCE] = $this->instantiateClass($className, $constructorArguments); return $this->objects[$objectName][self::KEY_INSTANCE]; } @@ -542,7 +537,7 @@ protected function buildObjectByFactory(string $objectName): object * Speed optimized alternative to ReflectionClass::newInstanceArgs() * * @param string $className Name of the class to instantiate - * @param array $arguments Arguments to pass to the constructor + * @param array $arguments Arguments to pass to the constructor * @return object The object * @throws Exception\CannotBuildObjectException * @throws \Exception @@ -553,14 +548,80 @@ protected function instantiateClass(string $className, array $arguments): object throw new Exception\CannotBuildObjectException('Circular dependency detected while trying to instantiate class "' . $className . '".', 1168505928); } + $this->classesBeingInstantiated[$className] = true; try { - $object = new $className(...$arguments); - unset($this->classesBeingInstantiated[$className]); - return $object; + return new $className(...$arguments); } catch (\Exception $exception) { - unset($this->classesBeingInstantiated[$className]); throw $exception; + } finally { + unset($this->classesBeingInstantiated[$className]); + } + } + + protected function autowireArguments($configuration, $existingArguments): array + { + foreach ($configuration as $index => $argument) { + if ($argument === null || $argument['wm'] === 0) { + continue; + } + $existingArguments[$index - 1] = $this->getConfiguredArgument($argument[self::KEY_ARGUMENT_TYPE], $argument[self::KEY_ARGUMENT_VALUE]); + } + + return $existingArguments; + } + + protected function getConfiguredArgument(int $argumentType, mixed $argumentValue): mixed + { + if ($argumentType === ObjectConfigurationArgument::ARGUMENT_TYPES_SETTING) { + return $this->get(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, $argumentValue); + } + + if ($argumentType === ObjectConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE) { + return $argumentValue; + } + + if ($argumentType !== ObjectConfigurationArgument::ARGUMENT_TYPES_OBJECT) { + return null; + } + + if (!$argumentValue instanceof ObjectConfiguration) { + if (str_contains($argumentValue, '.')) { + $argumentValue = $this->get(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, $argumentValue); + } + $argumentObjectConfiguration = $this->objects[$argumentValue] ?? null; + if ($argumentObjectConfiguration === null) { + throw new \RuntimeException('broken'); + } + return $this->get($argumentValue); + } + + if ($argumentValue->getFactoryObjectName()) { + $methodName = $argumentValue->getFactoryMethodName(); + return $this->get($argumentValue->getFactoryObjectName())->$methodName(...$this->buildMethodArguments($argumentValue->getFactoryArguments())); + } + + $argumentValueObjectName = $argumentValue->getObjectName(); + if ($this->objects[$argumentValueObjectName][self::KEY_SCOPE] === ObjectConfiguration::SCOPE_PROTOTYPE) { + return new $argumentValueObjectName(...$this->buildMethodArguments($argumentValue->getArguments())); + } + + return $this->get($argumentValueObjectName); + } + + /** + * @param array $argumentConfigurations + * @return array the (auto)wired arguments + */ + protected function buildMethodArguments(array $argumentConfigurations): array + { + $result = []; + foreach ($argumentConfigurations as $argument) { + if ($argument === null || $argument->getAutowiring() === 0) { + continue; + } + $result[] = $this->getConfiguredArgument($argument->getType(), $argument->getValue()); } + return $result; } /** diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/Compiler.php b/Neos.Flow/Classes/ObjectManagement/Proxy/Compiler.php index b54c92cd33..d77f6f4b49 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/Compiler.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/Compiler.php @@ -36,31 +36,16 @@ class Compiler */ public const ORIGINAL_CLASSNAME_SUFFIX = '_Original'; - /** - * @var CompileTimeObjectManager - */ - protected $objectManager; - - /** - * @var PhpFrontend - */ - protected $classesCache; - - /** - * @var ReflectionService - */ - protected $reflectionService; - /** * @var array */ - protected $proxyClasses = []; + protected array $proxyClasses = []; /** * Hardcoded list of Flow sub packages which must be immune proxying for security, technical or conceptual reasons. * @var array */ - protected $excludedSubPackages = ['Neos\Flow\Aop', 'Neos\Flow\Cor', 'Neos\Flow\Obj', 'Neos\Flow\Pac', 'Neos\Flow\Ref', 'Neos\Flow\Uti']; + protected array $excludedSubPackages = ['Neos\Flow\Aop', 'Neos\Flow\Cor', 'Neos\Flow\Obj', 'Neos\Flow\Pac', 'Neos\Flow\Ref', 'Neos\Flow\Uti']; /** * Length of the prefix that will be checked for exclusion of proxy building. @@ -75,44 +60,18 @@ class Compiler * * @var array */ - protected $storedProxyClasses = []; + protected array $storedProxyClasses = []; /** * Compiler constructor. */ - public function __construct() - { - $this->excludedSubPackagesLength = strlen('Neos\Flow') + 4; - } - - /** - * @param CompileTimeObjectManager $objectManager - * @return void - */ - public function injectObjectManager(CompileTimeObjectManager $objectManager): void - { - $this->objectManager = $objectManager; - } + public function __construct( + public PhpFrontend $classesCache, + public CompileTimeObjectManager $objectManager, + public ReflectionService $reflectionService - /** - * Injects the cache for storing the renamed original classes and proxy classes - * - * @param PhpFrontend $classesCache - * @return void - * @Flow\Autowiring(false) - */ - public function injectClassesCache(PhpFrontend $classesCache): void - { - $this->classesCache = $classesCache; - } - - /** - * @param ReflectionService $reflectionService - * @return void - */ - public function injectReflectionService(ReflectionService $reflectionService): void - { - $this->reflectionService = $reflectionService; + ) { + $this->excludedSubPackagesLength = strlen('Neos\Flow') + 4; } /** diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php index 06e5607701..4088950615 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 { + $foundEntity = false; 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); } } 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); + $foundEntity = true; } + + return $foundEntity; } /** @@ -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..39debefb33 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php @@ -28,58 +28,58 @@ class ProxyClass * * @var string */ - protected $namespace = ''; + protected string $namespace = ''; /** * The original class name * * @var string */ - protected $originalClassName; + protected string $originalClassName; /** * Fully qualified class name of the original class * * @var class-string */ - protected $fullOriginalClassName; + protected string $fullOriginalClassName; /** - * @var ProxyConstructorGenerator + * @var ProxyConstructorGenerator|null */ - protected $constructor; + protected ?ProxyConstructorGenerator $constructor = null; /** - * @var array + * @var array */ - protected $methods = []; + protected array $methods = []; /** - * @var array + * @var array */ - protected $constants = []; + protected array $constants = []; /** * Note: Not using ProxyInterface::class here, since the interface names must have a leading backslash. * - * @var array + * @var array */ - protected $interfaces = ['\Neos\Flow\ObjectManagement\Proxy\ProxyInterface']; + protected array $interfaces = ['\\' . ProxyInterface::class]; /** - * @var array + * @var array */ - protected $traits = []; + protected array $traits = []; /** - * @var array + * @var array */ - protected $properties = []; + protected array $properties = []; /** * @var ReflectionService */ - protected $reflectionService; + protected ReflectionService $reflectionService; /** * Creates a new ProxyClass instance. @@ -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] = [ @@ -186,7 +186,7 @@ public function addProperty(string $name, string $initialValueCode, string $visi * Note that the passed interface names must already have a leading backslash, * for example "\Neos\Flow\Foo\BarInterface". * - * @param array $interfaceNames Fully qualified names of the interfaces to introduce + * @param array $interfaceNames Fully qualified names of the interfaces to introduce * @return void */ public function addInterfaces(array $interfaceNames): void @@ -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..a2e389dae4 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php @@ -12,6 +12,7 @@ */ use Laminas\Code\Generator\DocBlockGenerator; +use Laminas\Code\Generator\ParameterGenerator; use Neos\Flow\ObjectManagement\DependencyInjection\ProxyClassBuilder; final class ProxyConstructorGenerator extends ProxyMethodGenerator @@ -56,6 +57,13 @@ public static function fromReflection(\Laminas\Code\Reflection\MethodReflection $docBlock->setWordWrap(false); $docBlock->setSourceDirty(false); $method->setDocBlock($docBlock); + + # Always include original parameters to support named arguments (issue #3076) + foreach ($reflectionMethod->getParameters() as $reflectionParameter) { + $parameter = ParameterGenerator::fromReflection($reflectionParameter); + $method->setParameter($parameter); + } + return $method; } @@ -92,7 +100,7 @@ public function renderBodyCode(): string protected function buildAssignMethodArgumentsCode(): string { - return '$arguments = func_get_args();' . PHP_EOL; + return ''; } /** @@ -122,7 +130,7 @@ protected function buildCallParentMethodCode(string $fullClassName, string $meth ) { return ''; } - return "parent::{$methodName}(...\$arguments);" . PHP_EOL; + return "parent::{$methodName}(...func_get_args());" . PHP_EOL; } /** 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/Caches.yaml b/Neos.Flow/Configuration/Caches.yaml index 480163797f..3b1de23102 100644 --- a/Neos.Flow/Configuration/Caches.yaml +++ b/Neos.Flow/Configuration/Caches.yaml @@ -103,17 +103,8 @@ Flow_Reflection_RuntimeData: Flow_Reflection_RuntimeClassSchemata: backend: Neos\Cache\Backend\SimpleFileBackend -# Flow_Resource_Status -# -# Stores the publication status of static and persistent resources -Flow_Resource_Status: - frontend: Neos\Cache\Frontend\StringFrontend - # Flow_Security_* Flow_Security_Authorization_Privilege_Method: [] -Flow_Security_Cryptography_RSAWallet: - backendOptions: - defaultLifetime: 30 Flow_Security_Cryptography_HashService: frontend: Neos\Cache\Frontend\StringFrontend backend: Neos\Cache\Backend\SimpleFileBackend diff --git a/Neos.Flow/Configuration/Objects.yaml b/Neos.Flow/Configuration/Objects.yaml index daae5adee2..fdc6decdb3 100644 --- a/Neos.Flow/Configuration/Objects.yaml +++ b/Neos.Flow/Configuration/Objects.yaml @@ -278,8 +278,8 @@ Neos\Flow\Mvc\Routing\RouterCachingService: value: Flow_Mvc_Routing_Resolve Neos\Flow\Mvc\ViewConfigurationManager: - properties: - cache: + arguments: + 3: object: factoryObjectName: Neos\Flow\Cache\CacheManager factoryMethodName: getCache @@ -301,6 +301,15 @@ Neos\Flow\ObjectManagement\ObjectManager: Neos\Flow\ObjectManagement\CompileTimeObjectManager: autowiring: off +Neos\Flow\ObjectManagement\Proxy\Compiler: + arguments: + 1: + object: + factoryObjectName: Neos\Flow\Cache\CacheManager + factoryMethodName: getCache + arguments: + 1: + value: Flow_Object_Classes # # # Persistence # # # @@ -349,20 +358,6 @@ Neos\Flow\Reflection\ReflectionService: #Neos\Flow\Reflection\ReflectionServiceFactory: # scope: singleton -# # -# ResourceManagement # -# # - -Neos\Flow\ResourceManagement\ResourceManager: - properties: - statusCache: - object: - factoryObjectName: Neos\Flow\Cache\CacheManager - factoryMethodName: getCache - arguments: - 1: - value: Flow_Resource_Status - # # # Security # # # @@ -374,14 +369,6 @@ Neos\Flow\Security\Authentication\AuthenticationManagerInterface: Neos\Flow\Security\Cryptography\RsaWalletServiceInterface: className: Neos\Flow\Security\Cryptography\RsaWalletServicePhp scope: singleton - properties: - keystoreCache: - object: - factoryObjectName: Neos\Flow\Cache\CacheManager - factoryMethodName: getCache - arguments: - 1: - value: Flow_Security_Cryptography_RSAWallet Neos\Flow\Security\Authorization\PrivilegeManagerInterface: className: Neos\Flow\Security\Authorization\PrivilegeManager @@ -467,6 +454,7 @@ Neos\Flow\Session\SessionInterface: factoryMethodName: getCurrentSession Neos\Flow\Session\Data\SessionKeyValueStore: + scope: singleton properties: cache: object: @@ -477,6 +465,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..e328a49d65 100644 --- a/Neos.Flow/Configuration/Settings.Object.yaml +++ b/Neos.Flow/Configuration/Settings.Object.yaml @@ -51,3 +51,13 @@ Neos: # - '^Neos\\SomePackage\\ValueObjects\\SomeSpecificValueObject$' # excludeClassesFromConstructorAutowiring: [] + + # This is a forward compatibility setting, preventing constructor arguments to be autowired + # if they are not declared singleton or session when this setting is false. + # + # 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. + # + # 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 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..df10bc94b6 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 } + prototypeAutowiring: + type: boolean diff --git a/Neos.Flow/Tests/Functional/ObjectManagement/DependencyInjectionTest.php b/Neos.Flow/Tests/Functional/ObjectManagement/DependencyInjectionTest.php index 712c5accae..d8e6e0a505 100644 --- a/Neos.Flow/Tests/Functional/ObjectManagement/DependencyInjectionTest.php +++ b/Neos.Flow/Tests/Functional/ObjectManagement/DependencyInjectionTest.php @@ -363,4 +363,62 @@ public function constructorSettingsInjectionViaInjectAnnotation(): void $object = new PrototypeClassL('override'); self::assertSame('override', $object->value); } + + /** + * Test that proxy constructors support PHP 8+ named arguments (issue #3076) + * + * @test + */ + public function prototypeObjectsCanBeInstantiatedWithNamedArguments(): void + { + $valueObject = new Fixtures\ValueObjectClassA('test-value'); + $object = new Fixtures\ClassWithNamedConstructorArguments( + valueObject: $valueObject, + stringValue: 'named-value' + ); + + self::assertSame($valueObject, $object->getValueObject()); + self::assertSame('named-value', $object->getStringValue()); + } + + /** + * Test that named arguments work with positional arguments + * + * @test + */ + public function prototypeObjectsCanBeInstantiatedWithPositionalArguments(): void + { + $valueObject = new Fixtures\ValueObjectClassA('test-value'); + $object = new Fixtures\ClassWithNamedConstructorArguments($valueObject, 'positional-value'); + + self::assertSame($valueObject, $object->getValueObject()); + self::assertSame('positional-value', $object->getStringValue()); + } + + /** + * Test that named arguments work with default values + * + * @test + */ + public function prototypeObjectsCanBeInstantiatedWithNamedArgumentsAndDefaults(): void + { + $valueObject = new Fixtures\ValueObjectClassA('test-value'); + $object = new Fixtures\ClassWithNamedConstructorArguments(valueObject: $valueObject); + + self::assertSame($valueObject, $object->getValueObject()); + self::assertSame('default', $object->getStringValue()); + } + + /** + * Test that constructor injection still works with named argument support + * + * @test + */ + public function singletonObjectsWithConstructorInjectionStillWorkWithNamedArgumentSupport(): void + { + $singleton = $this->objectManager->get(Fixtures\SingletonWithConstructorInjection::class); + + self::assertInstanceOf(Fixtures\SingletonClassA::class, $singleton->getSingletonA()); + self::assertNull($singleton->getOptionalValue()); + } } 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/ClassWithNamedConstructorArguments.php b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithNamedConstructorArguments.php new file mode 100644 index 0000000000..d7b5493e52 --- /dev/null +++ b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/ClassWithNamedConstructorArguments.php @@ -0,0 +1,38 @@ +valueObject; + } + + public function getStringValue(): string + { + return $this->stringValue; + } +} 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/SingletonWithConstructorInjection.php b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SingletonWithConstructorInjection.php new file mode 100644 index 0000000000..2fe96b1183 --- /dev/null +++ b/Neos.Flow/Tests/Functional/ObjectManagement/Fixtures/SingletonWithConstructorInjection.php @@ -0,0 +1,43 @@ +singletonA = $singletonA; + $this->optionalValue = $optionalValue; + } + + public function getSingletonA(): SingletonClassA + { + return $this->singletonA; + } + + public function getOptionalValue(): ?string + { + return $this->optionalValue; + } +} diff --git a/Neos.Flow/Tests/Functional/ObjectManagement/ProxySerializationTest.php b/Neos.Flow/Tests/Functional/ObjectManagement/ProxySerializationTest.php new file mode 100644 index 0000000000..284e64c3a0 --- /dev/null +++ b/Neos.Flow/Tests/Functional/ObjectManagement/ProxySerializationTest.php @@ -0,0 +1,39 @@ +someValue); + } +} diff --git a/Neos.Flow/Tests/Unit/Cli/CommandManagerTest.php b/Neos.Flow/Tests/Unit/Cli/CommandManagerTest.php index fc8a51b1f2..0fa61b1d2d 100644 --- a/Neos.Flow/Tests/Unit/Cli/CommandManagerTest.php +++ b/Neos.Flow/Tests/Unit/Cli/CommandManagerTest.php @@ -45,10 +45,12 @@ class CommandManagerTest extends UnitTestCase protected function setUp(): void { $this->mockReflectionService = $this->createMock(ReflectionService::class); - $this->commandManager = $this->getMockBuilder(Cli\CommandManager::class)->setMethods(['getAvailableCommands'])->getMock(); - $this->mockBootstrap = $this->getMockBuilder(Bootstrap::class)->disableOriginalConstructor()->getMock(); - $this->commandManager->injectBootstrap($this->mockBootstrap); + $this->mockObjectManager = $this->createMock(ObjectManagerInterface::class); + $this->mockObjectManager->method('get')->with([ReflectionService::class])->willReturn($this->mockReflectionService); + $this->commandManager = $this->getMockBuilder(Cli\CommandManager::class)->setMethods(['getAvailableCommands']) + ->setConstructorArgs([$this->mockBootstrap, $this->mockObjectManager]) + ->getMock(); } /** @@ -56,13 +58,13 @@ protected function setUp(): void */ public function getAvailableCommandsReturnsAllAvailableCommands(): void { - $commandManager = new CommandManager(); $mockCommandControllerClassNames = [Fixtures\Command\MockACommandController::class, Fixtures\Command\MockBCommandController::class]; $this->mockReflectionService->expects(self::once())->method('getAllSubClassNamesForClass')->with(Cli\CommandController::class)->willReturn($mockCommandControllerClassNames); $mockObjectManager = $this->createMock(ObjectManagerInterface::class); $mockObjectManager->method('get')->with(ReflectionService::class)->willReturn($this->mockReflectionService); $mockObjectManager->method('getObjectNameByClassName')->willReturnArgument(0); - $commandManager->injectObjectManager($mockObjectManager); + $mockBootstrap = $this->getMockBuilder(Bootstrap::class)->disableOriginalConstructor()->getMock(); + $commandManager = new CommandManager($mockBootstrap, $mockObjectManager); $commands = $commandManager->getAvailableCommands(); self::assertCount(3, $commands); diff --git a/Neos.Flow/Tests/Unit/Cli/RequestBuilderTest.php b/Neos.Flow/Tests/Unit/Cli/RequestBuilderTest.php index a54cf0003e..e2d9534169 100644 --- a/Neos.Flow/Tests/Unit/Cli/RequestBuilderTest.php +++ b/Neos.Flow/Tests/Unit/Cli/RequestBuilderTest.php @@ -15,9 +15,11 @@ use Neos\Flow\Mvc\Exception\InvalidArgumentMixingException; use Neos\Flow\Mvc\Exception\NoSuchCommandException; use Neos\Flow\ObjectManagement\ObjectManagerInterface; +use Neos\Flow\Package\PackageManager; use Neos\Flow\Reflection\ReflectionService; use Neos\Flow\Tests\UnitTestCase; use Neos\Flow\Cli; +use Neos\Flow\Utility\Environment; /** * Testcase for the MVC CLI Request Builder @@ -67,9 +69,10 @@ protected function setUp(): void $this->mockReflectionService = $this->createMock(ReflectionService::class); - $this->requestBuilder = new Cli\RequestBuilder(); - $this->requestBuilder->injectObjectManager($this->mockObjectManager); - $this->requestBuilder->injectCommandManager($this->mockCommandManager); + $mockEnvironment = $this->createMock(Environment::class); + $mockPackageManager = $this->createMock(PackageManager::class); + + $this->requestBuilder = new Cli\RequestBuilder($mockEnvironment, $this->mockObjectManager, $mockPackageManager, $this->mockCommandManager); } /** @@ -91,14 +94,7 @@ public function cliAccessWithPackageControllerAndActionNameBuildsCorrectRequest( */ public function ifCommandCantBeResolvedTheHelpScreenIsShown() { - // The following call is only made to satisfy PHPUnit. For some weird reason PHPUnit complains that the - // mocked method ("getObjectNameByClassName") does not exist _if the mock object is not used_. - $this->mockObjectManager->getObjectNameByClassName('Acme\Test\Command\DefaultCommandController'); - $this->mockCommandManager->getCommandByIdentifier('acme.test:default:list'); - - $mockCommandManager = $this->createMock(Cli\CommandManager::class); - $mockCommandManager->expects(self::any())->method('getCommandByIdentifier')->with('test:default:list')->will(self::throwException(new NoSuchCommandException())); - $this->requestBuilder->injectCommandManager($mockCommandManager); + $this->mockCommandManager->expects(self::any())->method('getCommandByIdentifier')->with('test:default:list')->will(self::throwException(new NoSuchCommandException())); $request = $this->requestBuilder->build('test:default:list'); self::assertSame(HelpCommandController::class, $request->getControllerObjectName()); diff --git a/Neos.Flow/Tests/Unit/Mvc/DispatchMiddlewareTest.php b/Neos.Flow/Tests/Unit/Mvc/DispatchMiddlewareTest.php index 3e89af1bc1..995b9bce5b 100644 --- a/Neos.Flow/Tests/Unit/Mvc/DispatchMiddlewareTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/DispatchMiddlewareTest.php @@ -56,7 +56,6 @@ class DispatchMiddlewareTest extends UnitTestCase */ protected function setUp(): void { - $this->dispatchMiddleware = new DispatchMiddleware(); $this->mockRequestHandler = $this->getMockBuilder(RequestHandlerInterface::class)->disableOriginalConstructor()->getMock(); $httpResponse = new Response(); @@ -67,7 +66,7 @@ protected function setUp(): void $this->mockHttpRequest->method('getUploadedFiles')->willReturn([]); $this->mockDispatcher = $this->getMockBuilder(Dispatcher::class)->getMock(); - $this->inject($this->dispatchMiddleware, 'dispatcher', $this->mockDispatcher); + $this->dispatchMiddleware = new DispatchMiddleware($this->mockDispatcher); $this->mockActionRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock(); $this->mockHttpRequest->method('getAttribute')->with(ServerRequestAttributes::ACTION_REQUEST)->willReturn($this->mockActionRequest); diff --git a/Neos.Flow/Tests/Unit/Mvc/ViewConfigurationManagerTest.php b/Neos.Flow/Tests/Unit/Mvc/ViewConfigurationManagerTest.php index 4744801a2f..f52e117d7c 100644 --- a/Neos.Flow/Tests/Unit/Mvc/ViewConfigurationManagerTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/ViewConfigurationManagerTest.php @@ -47,20 +47,15 @@ class ViewConfigurationManagerTest extends \Neos\Flow\Tests\UnitTestCase protected function setUp(): void { - $this->viewConfigurationManager = new ViewConfigurationManager(); - // eel evaluator $eelEvaluator = $this->createEvaluator(); - $this->inject($this->viewConfigurationManager, 'eelEvaluator', $eelEvaluator); // a dummy configuration manager is prepared $this->mockConfigurationManager = $this->getMockBuilder(ConfigurationManager::class)->disableOriginalConstructor()->getMock(); - $this->inject($this->viewConfigurationManager, 'configurationManager', $this->mockConfigurationManager); // caching is deactivated $this->mockCache = $this->getMockBuilder(VariableFrontend::class)->disableOriginalConstructor()->getMock(); $this->mockCache->expects(self::any())->method('get')->will(self::returnValue(false)); - $this->inject($this->viewConfigurationManager, 'cache', $this->mockCache); // a dummy request is prepared $this->mockActionRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock(); @@ -70,6 +65,8 @@ protected function setUp(): void $this->mockActionRequest->expects(self::any())->method('getControllerActionName')->will(self::returnValue('index')); $this->mockActionRequest->expects(self::any())->method('getFormat')->will(self::returnValue('html')); $this->mockActionRequest->expects(self::any())->method('getParentRequest')->will(self::returnValue(null)); + + $this->viewConfigurationManager = new ViewConfigurationManager($this->mockConfigurationManager, $eelEvaluator, $this->mockCache); } /** diff --git a/Neos.Flow/Tests/Unit/ObjectManagement/CompileTimeObjectManagerTest.php b/Neos.Flow/Tests/Unit/ObjectManagement/CompileTimeObjectManagerTest.php index 39aa65be6c..0e016f2cc2 100644 --- a/Neos.Flow/Tests/Unit/ObjectManagement/CompileTimeObjectManagerTest.php +++ b/Neos.Flow/Tests/Unit/ObjectManagement/CompileTimeObjectManagerTest.php @@ -11,6 +11,7 @@ * source code. */ +use Neos\Flow\Configuration\ConfigurationManager; use org\bovigo\vfs\vfsStream; use Neos\Flow\ObjectManagement\CompileTimeObjectManager; use Neos\Flow\Package\Package; @@ -34,9 +35,8 @@ protected function setUp(): void { vfsStream::setup('Packages'); $this->mockPackageManager = $this->getMockBuilder(PackageManager::class)->disableOriginalConstructor()->getMock(); - $this->compileTimeObjectManager = $this->getAccessibleMock(CompileTimeObjectManager::class, ['dummy'], [], '', false); - $this->compileTimeObjectManager->injectLogger($this->createMock(LoggerInterface::class)); - $configurations = [ + $mockConfigurationManager = $this->getMockBuilder(ConfigurationManager::class)->disableOriginalConstructor()->getMock(); + $mockConfigurationManager->method('getConfiguration')->willReturn([ 'Neos' => [ 'Flow' => [ 'object' => [ @@ -51,8 +51,10 @@ protected function setUp(): void ] ] ] - ]; - $this->compileTimeObjectManager->injectAllSettings($configurations); + ]); + $this->compileTimeObjectManager = $this->getAccessibleMock(CompileTimeObjectManager::class, ['dummy'], [], '', false); + $this->compileTimeObjectManager->injectLogger($this->createMock(LoggerInterface::class)); + $this->compileTimeObjectManager->injectConfigurationManager($mockConfigurationManager); } /** diff --git a/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationBuilderTest.php b/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationBuilderTest.php index 0aee7d0353..565d0bba43 100644 --- a/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationBuilderTest.php +++ b/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationBuilderTest.php @@ -11,17 +11,14 @@ * source code. */ -use Neos\Flow\Configuration\ConfigurationManager; -use Neos\Flow\ObjectManagement\Configuration\Configuration; -use Neos\Flow\ObjectManagement\Configuration\ConfigurationArgument; use Neos\Flow\ObjectManagement\Configuration\ConfigurationBuilder; -use Neos\Flow\ObjectManagement\Configuration\ConfigurationProperty; +use Neos\Flow\ObjectManagement\Configuration\ConfigurationParser; use Neos\Flow\ObjectManagement\Exception; -use Neos\Flow\ObjectManagement\Exception\InvalidObjectConfigurationException; use Neos\Flow\ObjectManagement\Exception\UnresolvedDependenciesException; use Neos\Flow\Reflection\ReflectionService; use Neos\Flow\Tests\UnitTestCase; use Neos\Flow\Annotations as Flow; +use Psr\Log\LoggerInterface; /** * Testcase for the object configuration builder @@ -29,106 +26,6 @@ */ class ConfigurationBuilderTest extends UnitTestCase { - /** - * @test - */ - public function allBasicOptionsAreSetCorrectly() - { - $factoryObjectName = 'ConfigurationBuilderTest' . md5(uniqid(mt_rand(), true)); - eval('class ' . $factoryObjectName . ' { public function manufacture() {} } '); - - $configurationArray = []; - $configurationArray['scope'] = 'prototype'; - $configurationArray['className'] = __CLASS__; - $configurationArray['factoryObjectName'] = $factoryObjectName; - $configurationArray['factoryMethodName'] = 'manufacture'; - $configurationArray['lifecycleInitializationMethodName'] = 'initializationMethod'; - $configurationArray['lifecycleShutdownMethodName'] = 'shutdownMethod'; - $configurationArray['autowiring'] = false; - - $objectConfiguration = new Configuration('TestObject', __CLASS__); - $objectConfiguration->setScope(Configuration::SCOPE_PROTOTYPE); - $objectConfiguration->setClassName(__CLASS__); - $objectConfiguration->setFactoryObjectName($factoryObjectName); - $objectConfiguration->setFactoryMethodName('manufacture'); - $objectConfiguration->setLifecycleInitializationMethodName('initializationMethod'); - $objectConfiguration->setLifecycleShutdownMethodName('shutdownMethod'); - $objectConfiguration->setAutowiring(Configuration::AUTOWIRING_MODE_OFF); - - $configurationBuilder = $this->getAccessibleMock(ConfigurationBuilder::class, ['dummy']); - $builtObjectConfiguration = $configurationBuilder->_call('parseConfigurationArray', 'TestObject', $configurationArray, __CLASS__); - self::assertEquals($objectConfiguration, $builtObjectConfiguration, 'The manually created and the built object configuration don\'t match.'); - } - - /** - * @test - */ - public function argumentsOfTypeObjectCanSpecifyAdditionalObjectConfigurationOptions() - { - $configurationArray = []; - $configurationArray['arguments'][1]['object']['name'] = 'Foo'; - $configurationArray['arguments'][1]['object']['className'] = __CLASS__; - - $argumentObjectConfiguration = new Configuration('Foo', __CLASS__); - $argumentObjectConfiguration->setConfigurationSourceHint(__CLASS__ . ', argument "1"'); - - $objectConfiguration = new Configuration('TestObject', 'TestObject'); - $objectConfiguration->setArgument(new ConfigurationArgument(1, $argumentObjectConfiguration, ConfigurationArgument::ARGUMENT_TYPES_OBJECT)); - - $configurationBuilder = $this->getAccessibleMock(ConfigurationBuilder::class, ['dummy']); - $builtObjectConfiguration = $configurationBuilder->_call('parseConfigurationArray', 'TestObject', $configurationArray, __CLASS__); - self::assertEquals($objectConfiguration, $builtObjectConfiguration); - } - - /** - * @test - */ - public function propertiesOfTypeObjectCanSpecifyAdditionalObjectConfigurationOptions() - { - $configurationArray = []; - $configurationArray['properties']['theProperty']['object']['name'] = 'Foo'; - $configurationArray['properties']['theProperty']['object']['className'] = __CLASS__; - - $propertyObjectConfiguration = new Configuration('Foo', __CLASS__); - $propertyObjectConfiguration->setConfigurationSourceHint(__CLASS__ . ', property "theProperty"'); - - $objectConfiguration = new Configuration('TestObject', 'TestObject'); - $objectConfiguration->setProperty(new ConfigurationProperty('theProperty', $propertyObjectConfiguration, ConfigurationProperty::PROPERTY_TYPES_OBJECT)); - - $configurationBuilder = $this->getAccessibleMock(ConfigurationBuilder::class, ['dummy']); - $builtObjectConfiguration = $configurationBuilder->_call('parseConfigurationArray', 'TestObject', $configurationArray, __CLASS__); - self::assertEquals($objectConfiguration, $builtObjectConfiguration); - } - - /** - * @test - */ - public function itIsPossibleToPassArraysAsStraightArgumentOrPropertyValues() - { - $configurationArray = []; - $configurationArray['properties']['straightValueProperty']['value'] = ['foo' => 'bar', 'object' => 'nö']; - $configurationArray['arguments'][1]['value'] = ['foo' => 'bar', 'object' => 'nö']; - - $objectConfiguration = new Configuration('TestObject', 'TestObject'); - $objectConfiguration->setProperty(new ConfigurationProperty('straightValueProperty', ['foo' => 'bar', 'object' => 'nö'])); - $objectConfiguration->setArgument(new ConfigurationArgument(1, ['foo' => 'bar', 'object' => 'nö'])); - - $configurationBuilder = $this->getAccessibleMock(ConfigurationBuilder::class, ['dummy']); - $builtObjectConfiguration = $configurationBuilder->_call('parseConfigurationArray', 'TestObject', $configurationArray, __CLASS__); - self::assertEquals($objectConfiguration, $builtObjectConfiguration); - } - - /** - * @test - */ - public function invalidOptionResultsInException() - { - $this->expectException(InvalidObjectConfigurationException::class); - $configurationArray = ['scoopy' => 'prototype']; - $configurationBuilder = $this->getAccessibleMock(ConfigurationBuilder::class, ['dummy']); - $configurationBuilder->_call('parseConfigurationArray', 'TestObject', $configurationArray, __CLASS__); - } - /** * @test */ @@ -139,9 +36,6 @@ public function privatePropertyAnnotatedForInjectionThrowsException() $configurationArray['arguments'][1]['setting'] = 'Neos.Foo.Bar'; $configurationArray['properties']['someProperty']['setting'] = 'Neos.Bar.Baz'; - $configurationBuilder = $this->getAccessibleMock(ConfigurationBuilder::class, ['dummy']); - $dummyObjectConfiguration = [$configurationBuilder->_call('parseConfigurationArray', __CLASS__, $configurationArray, __CLASS__)]; - $reflectionServiceMock = $this->createMock(ReflectionService::class); $reflectionServiceMock ->expects(self::once()) @@ -155,8 +49,10 @@ public function privatePropertyAnnotatedForInjectionThrowsException() ->with(__CLASS__, 'dummyProperty') ->will(self::returnValue(true)); - $configurationBuilder->injectReflectionService($reflectionServiceMock); - $configurationBuilder->_callRef('autowireProperties', $dummyObjectConfiguration); + $lopgerMock = $this->createMock(LoggerInterface::class); + + $configurationBuilder = new ConfigurationBuilder($reflectionServiceMock, new ConfigurationParser($reflectionServiceMock), $lopgerMock); + $configurationBuilder->buildObjectConfigurations(['Neos.Flow.Testing' => [__CLASS__]], ['Neos.Flow.Testing' => [__CLASS__ => $configurationArray]]); } /** @@ -175,40 +71,6 @@ public function errorOnGetClassMethodsThrowsException() $configurationBuilder->_callRef('autowireProperties', $dummyObjectConfiguration); } - /** - * @test - */ - public function parseConfigurationArrayBuildsConfigurationPropertyForInjectedSetting() - { - $configurationArray = []; - $configurationArray['properties']['someProperty']['setting'] = 'Neos.Foo.Bar'; - - /** @var ConfigurationBuilder $configurationBuilder */ - $configurationBuilder = $this->getAccessibleMock(ConfigurationBuilder::class, null); - /** @var Configuration $builtObjectConfiguration */ - $builtObjectConfiguration = $configurationBuilder->_call('parseConfigurationArray', 'TestObject', $configurationArray, __CLASS__); - - $expectedConfigurationProperty = new ConfigurationProperty('someProperty', ['type' => ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'path' => 'Neos.Foo.Bar'], ConfigurationProperty::PROPERTY_TYPES_CONFIGURATION); - self::assertEquals($expectedConfigurationProperty, $builtObjectConfiguration->getProperties()['someProperty']); - } - - /** - * @test - */ - public function parseConfigurationArrayBuildsConfigurationArgumentForInjectedSetting() - { - $configurationArray = []; - $configurationArray['arguments'][1]['setting'] = 'Neos.Foo.Bar'; - - /** @var ConfigurationBuilder $configurationBuilder */ - $configurationBuilder = $this->getAccessibleMock(ConfigurationBuilder::class, null); - /** @var Configuration $builtObjectConfiguration */ - $builtObjectConfiguration = $configurationBuilder->_call('parseConfigurationArray', 'TestObject', $configurationArray, __CLASS__); - - $expectedConfigurationArgument = new ConfigurationArgument(1, 'Neos.Foo.Bar', ConfigurationArgument::ARGUMENT_TYPES_SETTING); - self::assertEquals($expectedConfigurationArgument, $builtObjectConfiguration->getArguments()[1]); - } - /** * @test */ diff --git a/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationParserTest.php b/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationParserTest.php new file mode 100644 index 0000000000..10763d8cb9 --- /dev/null +++ b/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationParserTest.php @@ -0,0 +1,155 @@ +setScope(Configuration::SCOPE_PROTOTYPE); + $objectConfiguration->setClassName(__CLASS__); + $objectConfiguration->setFactoryObjectName($factoryObjectName); + $objectConfiguration->setFactoryMethodName('manufacture'); + $objectConfiguration->setLifecycleInitializationMethodName('initializationMethod'); + $objectConfiguration->setLifecycleShutdownMethodName('shutdownMethod'); + $objectConfiguration->setAutowiring(Configuration::AUTOWIRING_MODE_OFF); + + $reflectionServiceMock = $this->getMockBuilder(ReflectionService::class)->getMock(); + $configurationParser = new ConfigurationParser($reflectionServiceMock); + $builtObjectConfiguration = $configurationParser->parseConfigurationArray('TestObject', $configurationArray, __CLASS__); + self::assertEquals($objectConfiguration, $builtObjectConfiguration); + } + + /** + * @test + */ + public function argumentsOfTypeObjectCanSpecifyAdditionalObjectConfigurationOptions() + { + $configurationArray = []; + $configurationArray['arguments'][1]['object']['name'] = 'Foo'; + $configurationArray['arguments'][1]['object']['className'] = __CLASS__; + + $argumentObjectConfiguration = new Configuration('Foo', __CLASS__); + $argumentObjectConfiguration->setConfigurationSourceHint(__CLASS__ . ', argument "1"'); + + $objectConfiguration = new Configuration('TestObject', 'TestObject'); + $objectConfiguration->setArgument(new ConfigurationArgument(1, $argumentObjectConfiguration, ConfigurationArgument::ARGUMENT_TYPES_OBJECT)); + + $reflectionServiceMock = $this->getMockBuilder(ReflectionService::class)->getMock(); + $configurationParser = new ConfigurationParser($reflectionServiceMock); + $builtObjectConfiguration = $configurationParser->parseConfigurationArray('TestObject', $configurationArray, __CLASS__); + self::assertEquals($objectConfiguration, $builtObjectConfiguration); + } + + /** + * @test + */ + public function propertiesOfTypeObjectCanSpecifyAdditionalObjectConfigurationOptions() + { + $configurationArray = []; + $configurationArray['properties']['theProperty']['object']['name'] = 'Foo'; + $configurationArray['properties']['theProperty']['object']['className'] = __CLASS__; + + $propertyObjectConfiguration = new Configuration('Foo', __CLASS__); + $propertyObjectConfiguration->setConfigurationSourceHint(__CLASS__ . ', property "theProperty"'); + + $objectConfiguration = new Configuration('TestObject', 'TestObject'); + $objectConfiguration->setProperty(new ConfigurationProperty('theProperty', $propertyObjectConfiguration, ConfigurationProperty::PROPERTY_TYPES_OBJECT)); + + $reflectionServiceMock = $this->getMockBuilder(ReflectionService::class)->getMock(); + $configurationParser = new ConfigurationParser($reflectionServiceMock); + $builtObjectConfiguration = $configurationParser->parseConfigurationArray('TestObject', $configurationArray, __CLASS__); + self::assertEquals($objectConfiguration, $builtObjectConfiguration); + } + + /** + * @test + */ + public function itIsPossibleToPassArraysAsStraightArgumentOrPropertyValues() + { + $configurationArray = []; + $configurationArray['properties']['straightValueProperty']['value'] = ['foo' => 'bar', 'object' => 'nö']; + $configurationArray['arguments'][1]['value'] = ['foo' => 'bar', 'object' => 'nö']; + + $objectConfiguration = new Configuration('TestObject', 'TestObject'); + $objectConfiguration->setProperty(new ConfigurationProperty('straightValueProperty', ['foo' => 'bar', 'object' => 'nö'])); + $objectConfiguration->setArgument(new ConfigurationArgument(1, ['foo' => 'bar', 'object' => 'nö'])); + + $reflectionServiceMock = $this->getMockBuilder(ReflectionService::class)->getMock(); + $configurationParser = new ConfigurationParser($reflectionServiceMock); + $builtObjectConfiguration = $configurationParser->parseConfigurationArray('TestObject', $configurationArray, __CLASS__); + self::assertEquals($objectConfiguration, $builtObjectConfiguration); + } + + /** + * @test + */ + public function invalidOptionResultsInException() + { + $this->expectException(InvalidObjectConfigurationException::class); + $configurationArray = ['scoopy' => 'prototype']; + $reflectionServiceMock = $this->getMockBuilder(ReflectionService::class)->getMock(); + $configurationParser = new ConfigurationParser($reflectionServiceMock); + $configurationParser->parseConfigurationArray('TestObject', $configurationArray, __CLASS__); + } + + /** + * @test + */ + public function parseConfigurationArrayBuildsConfigurationArgumentForInjectedSetting() + { + $configurationArray = []; + $configurationArray['arguments'][1]['setting'] = 'Neos.Foo.Bar'; + + $reflectionServiceMock = $this->getMockBuilder(ReflectionService::class)->getMock(); + $configurationParser = new ConfigurationParser($reflectionServiceMock); + $builtObjectConfiguration = $configurationParser->parseConfigurationArray('TestObject', $configurationArray, __CLASS__); + + $expectedConfigurationArgument = new ConfigurationArgument(1, 'Neos.Foo.Bar', ConfigurationArgument::ARGUMENT_TYPES_SETTING); + self::assertEquals($expectedConfigurationArgument, $builtObjectConfiguration->getArguments()[1]); + } + + /** + * @test + */ + public function parseConfigurationArrayBuildsConfigurationPropertyForInjectedSetting() + { + $configurationArray = []; + $configurationArray['properties']['someProperty']['setting'] = 'Neos.Foo.Bar'; + + $reflectionServiceMock = $this->getMockBuilder(ReflectionService::class)->getMock(); + $configurationParser = new ConfigurationParser($reflectionServiceMock); + $builtObjectConfiguration = $configurationParser->parseConfigurationArray('TestObject', $configurationArray, __CLASS__); + + $expectedConfigurationProperty = new ConfigurationProperty('someProperty', ['type' => ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'path' => 'Neos.Foo.Bar'], ConfigurationProperty::PROPERTY_TYPES_CONFIGURATION); + self::assertEquals($expectedConfigurationProperty, $builtObjectConfiguration->getProperties()['someProperty']); + } +} diff --git a/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationTest.php b/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationTest.php index d7909b649a..180e3afdea 100644 --- a/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationTest.php +++ b/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationTest.php @@ -31,23 +31,7 @@ class ConfigurationTest extends UnitTestCase */ protected function setUp(): void { - $this->objectConfiguration = new Configuration\Configuration('Neos\Foo\Bar'); - } - - /** - * Checks if setProperties accepts only valid values - * - * @test - */ - public function setPropertiesOnlyAcceptsValidValues() - { - $this->expectException(InvalidConfigurationException::class); - $invalidProperties = [ - 'validProperty' => new Configuration\ConfigurationProperty('validProperty', 'simple string'), - 'invalidProperty' => 'foo' - ]; - - $this->objectConfiguration->setProperties($invalidProperties); + $this->objectConfiguration = new Configuration\Configuration('Neos\Foo\Bar', 'Neos\Foo\Bar'); } /** @@ -66,22 +50,6 @@ public function passingAnEmptyArrayToSetPropertiesRemovesAllExistingproperties() self::assertEquals([], $this->objectConfiguration->getProperties(), 'The properties have not been cleared.'); } - /** - * Checks if setArguments accepts only valid values - * - * @test - */ - public function setArgumentsOnlyAcceptsValidValues() - { - $this->expectException(InvalidConfigurationException::class); - $invalidArguments = [ - 1 => new Configuration\ConfigurationArgument(1, 'simple string'), - 2 => 'foo' - ]; - - $this->objectConfiguration->setArguments($invalidArguments); - } - /** * @test */ @@ -116,15 +84,6 @@ public function setFactoryMethodNameAcceptsValidStrings() self::assertSame('someMethodName', $this->objectConfiguration->getFactoryMethodName()); } - /** - * @test - */ - public function setFactoryMethodNameRejectsAnythingElseThanAString() - { - $this->expectException(\InvalidArgumentException::class); - $this->objectConfiguration->setFactoryMethodName([]); - } - /** * @test */ diff --git a/Neos.Flow/Tests/Unit/ObjectManagement/Proxy/CompilerTest.php b/Neos.Flow/Tests/Unit/ObjectManagement/Proxy/CompilerTest.php index 4b5b77d54f..840b5e0413 100644 --- a/Neos.Flow/Tests/Unit/ObjectManagement/Proxy/CompilerTest.php +++ b/Neos.Flow/Tests/Unit/ObjectManagement/Proxy/CompilerTest.php @@ -13,12 +13,16 @@ require_once(__DIR__ . '/../Fixture/FooBarAnnotation.php'); +use Neos\Cache\Frontend\PhpFrontend; use Neos\Flow\Annotations\Inject; use Neos\Flow\Annotations\Scope; use Neos\Flow\Annotations\Session; use Neos\Flow\Annotations\Signal; use Neos\Flow\Annotations\Validate; +use Neos\Flow\ObjectManagement\CompileTimeObjectManager; +use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\ObjectManagement\Proxy\Compiler; +use Neos\Flow\Reflection\ReflectionService; use Neos\Flow\Tests\Unit\ObjectManagement\Fixture\FooBarAnnotation; use Neos\Flow\Tests\UnitTestCase; use PHPUnit\Framework\MockObject\MockObject; @@ -35,7 +39,10 @@ class CompilerTest extends UnitTestCase protected function setUp(): void { - $this->compiler = $this->getAccessibleMock(Compiler::class, null); + $cacheMock = $this->getMockBuilder(PhpFrontend::class)->disableOriginalConstructor()->getMock(); + $objectManagerMock = $this->getMockBuilder(CompileTimeObjectManager::class)->disableOriginalConstructor()->getMock(); + $reflectionserviceMock = $this->getMockBuilder(ReflectionService::class)->getMock(); + $this->compiler = $this->getAccessibleMock(Compiler::class, null, [], '', false); } public function annotationsAndStrings(): array