From fc8f8779e4b9daf17e664c76d5a36b42ff1c4c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Mon, 25 May 2026 20:05:09 +0200 Subject: [PATCH 1/2] BUGFIX: Allow smooth upgrades from var tags Any property that gets reflected for "var" via `ReflectionService::getPropertyTagsValues` cannot sensibly be upgraded to use type hints only. This addition gives a sensible upgrade path by falling back to reflected type if no "var" tag was available. In the long run this allows code to seamlessly change from var tags to type hints in all places. --- .../Classes/Reflection/ReflectionService.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Neos.Flow/Classes/Reflection/ReflectionService.php b/Neos.Flow/Classes/Reflection/ReflectionService.php index 79bf710754..8dcfb95986 100644 --- a/Neos.Flow/Classes/Reflection/ReflectionService.php +++ b/Neos.Flow/Classes/Reflection/ReflectionService.php @@ -846,7 +846,22 @@ public function getPropertyTagsValues(string $className, string $propertyName): public function getPropertyTagValues(string $className, string $propertyName, string $tag): array { $className = $this->prepareClassReflectionForUsage($className); - return $this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_TAGS_VALUES][$tag] ?? []; + $tagValues = $this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_TAGS_VALUES][$tag] ?? []; + if ($tag !== 'var') { + return $tagValues; + } + + if ($tagValues !== []) { + return $tagValues; + } + + // Upgrade behavior for "var" tag requests also considering the type hints as fallback. + $propertyType = $this->getPropertyType($className, $propertyName); + if ($propertyType !== null) { + return [$propertyType]; + } + + return []; } /** From 0352fe2417594df0358da554707efe36132bb5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Fri, 29 May 2026 22:45:30 +0200 Subject: [PATCH 2/2] TASK: getPropertyTagValues fallback to getPropertyType This reverts the original commit adding the fallback to getPropertyTagValues and instead does the fallback at every place that uses getPropertyTagValues in the framework. --- .../Classes/Aop/Builder/ProxyClassBuilder.php | 4 ++ .../Configuration/ConfigurationBuilder.php | 53 ++++++++++++++----- .../TypeConverter/ObjectConverter.php | 7 ++- .../Classes/Validation/ValidatorResolver.php | 20 +++---- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php index 757b0d72b6..c1dccd1c33 100644 --- a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php @@ -398,6 +398,10 @@ public function buildProxyClass(string $targetClassName, array $aspectContainers $propertyName = $propertyIntroduction->getPropertyName(); $declaringAspectClassName = $propertyIntroduction->getDeclaringAspectClassName(); $possiblePropertyTypes = $this->reflectionService->getPropertyTagValues($declaringAspectClassName, $propertyName, 'var'); + if (count($possiblePropertyTypes) === 0) { + $typeHint = $this->reflectionService->getPropertyType($declaringAspectClassName, $propertyName); + $possiblePropertyTypes = $typeHint !== null ? [$typeHint] : []; + } if (count($possiblePropertyTypes) > 0 && !$this->reflectionService->isPropertyAnnotatedWith($declaringAspectClassName, $propertyName, Flow\Transient::class)) { $classSchema = $this->reflectionService->getClassSchema($targetClassName); $classSchema?->addProperty($propertyName, $possiblePropertyTypes[0]); diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php index 79dc0550d0..318b9b59d9 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php @@ -339,20 +339,8 @@ protected static function parseAutowiring($value) 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]; - } - } + $objectName = $this->determineObjectNameOfProperty($objectNameOrConfiguration, $parentObjectConfiguration->getClassName(), $propertyName, $parentObjectConfiguration->getConfigurationSourceHint()); + unset($objectNameOrConfiguration['name']); $objectConfiguration = $this->parseConfigurationArray($objectName, $objectNameOrConfiguration, $parentObjectConfiguration->getConfigurationSourceHint() . ', property "' . $propertyName . '"'); $property = new ConfigurationProperty($propertyName, $objectConfiguration, ConfigurationProperty::PROPERTY_TYPES_OBJECT); } else { @@ -361,6 +349,42 @@ protected function parsePropertyOfTypeObject($propertyName, $objectNameOrConfigu return $property; } + /** + * @param $objectNameOrConfiguration + * @param $parentClassName + * @param $propertyName + * @param string $configurationSourceHint + * @return string + * @throws InvalidObjectConfigurationException + * @throws \Neos\Flow\Reflection\Exception\ClassLoadingForReflectionFailedException + * @throws \Neos\Flow\Reflection\Exception\InvalidClassException + * @throws \ReflectionException + */ + protected function determineObjectNameOfProperty($objectNameOrConfiguration, $parentClassName, $propertyName, string $configurationSourceHint): string + { + if (isset($objectNameOrConfiguration['name'])) { + return $objectNameOrConfiguration['name']; + } + + if (isset($objectNameOrConfiguration['factoryObjectName']) || isset($objectNameOrConfiguration['factoryMethodName'])) { + return ''; + } + + $varTags = $this->reflectionService->getPropertyTagValues($parentClassName, $propertyName, 'var'); + if (count($varTags) > 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.', $parentClassName, $configurationSourceHint, $propertyName), 1297097815); + } + if (count($varTags) === 1) { + return $varTags[0]; + } + $typeHint = $this->reflectionService->getPropertyType($parentClassName, $propertyName); + if ($typeHint === null) { + throw new InvalidObjectConfigurationException(sprintf('Object %s (%s), for property "%s", contains neither object name, nor factory object name, and neither is the property properly type hinted or @var - tagged.', $parentClassName, $configurationSourceHint, $propertyName), 1779793488); + } + + return $typeHint; + } + /** * Parses the configuration for arguments of type OBJECT * @@ -590,6 +614,7 @@ protected function autowireProperties(array &$objectConfigurations) $enableLazyInjection = false; # See: https://github.com/neos/flow-development-collection/issues/2114 } } + // This is already a fallback, getPropertyType is used first. if ($objectName === null) { $objectName = trim(implode('', $this->reflectionService->getPropertyTagValues($className, $propertyName, 'var')), ' \\'); } diff --git a/Neos.Flow/Classes/Property/TypeConverter/ObjectConverter.php b/Neos.Flow/Classes/Property/TypeConverter/ObjectConverter.php index 0c350a3cfa..4e6f5726c1 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/ObjectConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/ObjectConverter.php @@ -156,8 +156,13 @@ public function getTypeOfChildProperty($targetType, $propertyName, PropertyMappi } return $parsedType['type'] . ($parsedType['elementType'] !== null ? '<' . $parsedType['elementType'] . '>' : ''); } + $typeHint = $this->reflectionService->getPropertyType($targetType, $propertyName); + if ($typeHint !== null) { + $parsedType = TypeHandling::parseType($typeHint); + return $parsedType['type']; + } - throw new InvalidTargetException(sprintf('Public property "%s" had no proper type annotation (i.e. "@var") in target object of type "%s".', $propertyName, $targetType), 1406821818); + throw new InvalidTargetException(sprintf('Public property "%s" had no type hints or proper type tag (i.e. "@var") in target object of type "%s".', $propertyName, $targetType), 1406821818); } } diff --git a/Neos.Flow/Classes/Validation/ValidatorResolver.php b/Neos.Flow/Classes/Validation/ValidatorResolver.php index 54ff349e36..03efe0aaf9 100644 --- a/Neos.Flow/Classes/Validation/ValidatorResolver.php +++ b/Neos.Flow/Classes/Validation/ValidatorResolver.php @@ -296,20 +296,17 @@ protected function buildBaseValidatorConjunction($indexKey, $targetClassName, ar } $conjunctionValidator->addValidator($objectValidator); foreach ($this->reflectionService->getClassPropertyNames($targetClassName) as $classPropertyName) { + if ($this->reflectionService->isPropertyAnnotatedWith($targetClassName, $classPropertyName, Flow\IgnoreValidation::class)) { + continue; + } + $classPropertyTagsValues = $this->reflectionService->getPropertyTagsValues($targetClassName, $classPropertyName); if (!isset($classPropertyTagsValues['var'])) { - try { - $propertyReflection = new PropertyReflection($targetClassName, $classPropertyName); - } catch (\ReflectionException $e) { - throw new \RuntimeException(sprintf('Failed reflecting property %s from class %s while building base a validator conjunction: %s', $classPropertyName, $targetClassName, $e->getMessage()), 1651570561); - } - - if (!$propertyReflection->hasType()) { + $type = $this->reflectionService->getPropertyType($targetClassName, $classPropertyName); + if ($type === null) { throw new \InvalidArgumentException(sprintf('Failed building base validator conjunction for property %s in class %s because there is no @var annotation and no type declaration.', $classPropertyName, $targetClassName), 1363778104); } - /** @var \ReflectionNamedType $type */ - $type = $propertyReflection->getType(); - $classPropertyTagsValues['var'][] = $type->getName(); + $classPropertyTagsValues['var'][] = $this->reflectionService->getPropertyType($targetClassName, $classPropertyName); } try { $parsedType = TypeHandling::parseType(trim(implode('', $classPropertyTagsValues['var']), ' \\')); @@ -317,9 +314,6 @@ protected function buildBaseValidatorConjunction($indexKey, $targetClassName, ar throw new \InvalidArgumentException(sprintf(' @var annotation of ' . $exception->getMessage(), 'class "' . $targetClassName . '", property "' . $classPropertyName . '"'), 1315564744, $exception); } - if ($this->reflectionService->isPropertyAnnotatedWith($targetClassName, $classPropertyName, Flow\IgnoreValidation::class)) { - continue; - } if ($classSchema !== null && $classSchema->hasProperty($classPropertyName) && $classSchema->isPropertyTransient($classPropertyName)