diff --git a/composer.json b/composer.json index 086fc39ae02dd00a04b95cc06d98a279826d6340..8dd193e5ba2442cba0e1d3941e1e74b974a1acf0 100644 --- a/composer.json +++ b/composer.json @@ -56,6 +56,7 @@ "symfony/finder": "^4.1", "symfony/polyfill-intl-icu": "^1.6", "symfony/polyfill-mbstring": "^1.2", + "symfony/property-access": "^4.2", "symfony/property-info": "^4.2", "symfony/routing": "^4.1", "symfony/yaml": "^4.1", diff --git a/composer.lock b/composer.lock index b3c65e42d177b2dc63e38ffa24998a49d7eeadca..59cbe890fdb4ea0c96fdd85bb1f6efd7bf9fabdd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "665468d5ffefa8e2e72be0a338838765", + "content-hash": "ae4664b4ee7bf0aa9a614d355d67db78", "packages": [ { "name": "cogpowered/finediff", @@ -2246,6 +2246,73 @@ ], "time": "2018-08-06T14:22:27+00:00" }, + { + "name": "symfony/property-access", + "version": "v4.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "a21d40670000f61a1a4b90a607d54696aad914cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/a21d40670000f61a1a4b90a607d54696aad914cd", + "reference": "a21d40670000f61a1a4b90a607d54696aad914cd", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/inflector": "~3.4|~4.0" + }, + "require-dev": { + "symfony/cache": "~3.4|~4.0" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony PropertyAccess Component", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property path", + "reflection" + ], + "time": "2019-01-05T16:37:49+00:00" + }, { "name": "symfony/property-info", "version": "v4.2.2", diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-87332-AvoidRuntimeReflectionCallsInObjectAccess.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-87332-AvoidRuntimeReflectionCallsInObjectAccess.rst new file mode 100644 index 0000000000000000000000000000000000000000..9b29faacec63a07537534a0bb7139e4801678796 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-87332-AvoidRuntimeReflectionCallsInObjectAccess.rst @@ -0,0 +1,40 @@ +.. include:: ../../Includes.txt + +==================================================================== +Deprecation: #87332 - Avoid runtime reflection calls in ObjectAccess +==================================================================== + +See :issue:`87332` + +Description +=========== + +1) Class :php:`\TYPO3\CMS\Extbase\Reflection\ObjectAccess` uses reflection to make non public properties gettable and settable. This behaviour is triggered by setting the argument :php:`$forceDirectAccess` of methods :php:`getProperty`, :php:`getPropertyInternal` or :php:`setProperty` to :php:`true`. Triggering this behaviour is deprecated and will be removed in TYPO3 11.0. + +2) Method :php:`\TYPO3\CMS\Extbase\Reflection\ObjectAccess::buildSetterMethodName` has been deprecated and will be removed in TYPO3 11.0. + + +Impact +====== + +1) Accessing non public properties via the mentioned methods will no longer work in TYPO3 11.0. + +2) Calling :php:`\TYPO3\CMS\Extbase\Reflection\ObjectAccess::buildSetterMethodName` will no longer work in TYPO3 11.0. + + +Affected Installations +====================== + +1) All installations that use the mentioned methods with argument :php:`$forceDirectAccess` set to :php:`true`. + +2) All installations that call :php:`\TYPO3\CMS\Extbase\Reflection\ObjectAccess::buildSetterMethodName`. + + +Migration +========= + +1) Make sure the affected property is accessible by either making it public or providing getters/hassers/issers or setters (:php:`getProperty()`, :php:`hasProperty()`, :php:`isProperty()`, :php:`setProperty()`). + +2) Build setter names manually: :php:`$setterMethodName = 'set' . ucfirst($propertyName);` + +.. index:: PHP-API, FullyScanned, ext:extbase diff --git a/typo3/sysext/extbase/Classes/Property/TypeConverter/ObjectConverter.php b/typo3/sysext/extbase/Classes/Property/TypeConverter/ObjectConverter.php index 8f01d76c594e2db2a1bc02608cd69bedbf49d122..0d6e9d7148bedfc700412d370faab109a8d09c1d 100644 --- a/typo3/sysext/extbase/Classes/Property/TypeConverter/ObjectConverter.php +++ b/typo3/sysext/extbase/Classes/Property/TypeConverter/ObjectConverter.php @@ -16,7 +16,6 @@ namespace TYPO3\CMS\Extbase\Property\TypeConverter; use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodException; use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodParameterException; -use TYPO3\CMS\Extbase\Reflection\ClassSchema\MethodParameter; /** * This converter transforms arrays to simple objects (POPO) by setting properties. @@ -122,9 +121,9 @@ class ObjectConverter extends AbstractTypeConverter $specificTargetType = $this->objectContainer->getImplementationClassName($targetType); $classSchema = $this->reflectionService->getClassSchema($specificTargetType); - if ($classSchema->hasMethod(\TYPO3\CMS\Extbase\Reflection\ObjectAccess::buildSetterMethodName($propertyName))) { - $methodParameters = $classSchema->getMethod(\TYPO3\CMS\Extbase\Reflection\ObjectAccess::buildSetterMethodName($propertyName))->getParameters(); - /** @var MethodParameter $methodParameter */ + $methodName = 'set' . ucfirst($propertyName); + if ($classSchema->hasMethod($methodName)) { + $methodParameters = $classSchema->getMethod($methodName)->getParameters() ?? []; $methodParameter = current($methodParameters); if ($methodParameter->getType() === null) { throw new \TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException('Setter for property "' . $propertyName . '" had no type hint or documentation in target object of type "' . $specificTargetType . '".', 1303379158); diff --git a/typo3/sysext/extbase/Classes/Reflection/ClassSchema.php b/typo3/sysext/extbase/Classes/Reflection/ClassSchema.php index 66e3dd8acffc026927075b441b521dea131a7507..4aa48608c7219f2d1b062d3f16262654aceb2cbc 100644 --- a/typo3/sysext/extbase/Classes/Reflection/ClassSchema.php +++ b/typo3/sysext/extbase/Classes/Reflection/ClassSchema.php @@ -560,7 +560,7 @@ class ClassSchema } /** - * @return array + * @return array|Method[] */ public function getMethods(): array { diff --git a/typo3/sysext/extbase/Classes/Reflection/ObjectAccess.php b/typo3/sysext/extbase/Classes/Reflection/ObjectAccess.php index aa15bab73c70507a2853f40dbd0136b03ec33ae8..dd5699d79bb4ea95da8931765eb386b8e36accd5 100644 --- a/typo3/sysext/extbase/Classes/Reflection/ObjectAccess.php +++ b/typo3/sysext/extbase/Classes/Reflection/ObjectAccess.php @@ -1,4 +1,6 @@ <?php +declare(strict_types = 1); + namespace TYPO3\CMS\Extbase\Reflection; /* @@ -14,6 +16,11 @@ namespace TYPO3\CMS\Extbase\Reflection; * The TYPO3 project - inspiring people to share! */ +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyPath; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\StringUtility; use TYPO3\CMS\Extbase\Persistence\ObjectStorage; /** @@ -27,11 +34,10 @@ use TYPO3\CMS\Extbase\Persistence\ObjectStorage; */ class ObjectAccess { - const ACCESS_GET = 0; - - const ACCESS_SET = 1; - - const ACCESS_PUBLIC = 2; + /** + * @var PropertyAccessor + */ + private static $propertyAccessor; /** * Get a property of a given object. @@ -49,16 +55,18 @@ class ObjectAccess * @param bool $forceDirectAccess directly access property using reflection(!) * * @throws \InvalidArgumentException in case $subject was not an object or $propertyName was not a string + * @throws Exception\PropertyNotAccessibleException * @return mixed Value of the property */ - public static function getProperty($subject, $propertyName, $forceDirectAccess = false) + public static function getProperty($subject, string $propertyName, bool $forceDirectAccess = false) { if (!is_object($subject) && !is_array($subject)) { - throw new \InvalidArgumentException('$subject must be an object or array, ' . gettype($subject) . ' given.', 1237301367); - } - if (!is_string($propertyName) && (!is_array($subject) && !$subject instanceof \ArrayAccess)) { - throw new \InvalidArgumentException('Given property name is not of type string.', 1231178303); + throw new \InvalidArgumentException( + '$subject must be an object or array, ' . gettype($subject) . ' given.', + 1237301367 + ); } + return self::getPropertyInternal($subject, $propertyName, $forceDirectAccess); } @@ -77,52 +85,38 @@ class ObjectAccess * @return mixed Value of the property * @internal */ - public static function getPropertyInternal($subject, $propertyName, $forceDirectAccess = false) + public static function getPropertyInternal($subject, string $propertyName, bool $forceDirectAccess = false) { - // type check and conversion of iterator to numerically indexed array - if ($subject === null || is_scalar($subject)) { - return null; + if ($forceDirectAccess === true) { + trigger_error('Argument $forceDirectAccess will be removed in TYPO3 11.0', E_USER_DEPRECATED); } + if (!$forceDirectAccess && ($subject instanceof \SplObjectStorage || $subject instanceof ObjectStorage)) { $subject = iterator_to_array(clone $subject, false); } - // value get based on data type of $subject (possibly converted above) - if (($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)) || is_array($subject)) { - // isset() is safe; array_key_exists would only be needed to determine - // if the value is NULL - and that's already what we return as fallback. - if (isset($subject[$propertyName])) { - return $subject[$propertyName]; - } - } elseif (is_object($subject)) { - if ($forceDirectAccess) { - if (property_exists($subject, $propertyName)) { - $propertyReflection = new \ReflectionProperty($subject, $propertyName); - if ($propertyReflection->isPublic()) { - return $propertyReflection->getValue($subject); - } - $propertyReflection->setAccessible(true); - return $propertyReflection->getValue($subject); - } - throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1302855001); - } - $upperCasePropertyName = ucfirst($propertyName); - $getterMethodName = 'get' . $upperCasePropertyName; - if (is_callable([$subject, $getterMethodName])) { - return $subject->{$getterMethodName}(); - } - $getterMethodName = 'is' . $upperCasePropertyName; - if (is_callable([$subject, $getterMethodName])) { - return $subject->{$getterMethodName}(); - } - $getterMethodName = 'has' . $upperCasePropertyName; - if (is_callable([$subject, $getterMethodName])) { - return $subject->{$getterMethodName}(); - } - if (property_exists($subject, $propertyName)) { - return $subject->{$propertyName}; + $propertyPath = new PropertyPath($propertyName); + + if ($subject instanceof \ArrayAccess) { + $accessor = self::createAccessor(); + + // Check if $subject is an instance of \ArrayAccess and therefore maybe has actual accessible properties. + if ($accessor->isReadable($subject, $propertyPath)) { + return $accessor->getValue($subject, $propertyPath); } - throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1476109666); + + // Use array style property path for instances of \ArrayAccess + // https://symfony.com/doc/current/components/property_access.html#reading-from-arrays + + $propertyPath = self::convertToArrayPropertyPath($propertyPath); + } + + if (is_object($subject)) { + return self::getObjectPropertyValue($subject, $propertyPath, $forceDirectAccess); + } + + if (is_array($subject)) { + return self::getArrayIndexValue($subject, self::convertToArrayPropertyPath($propertyPath)); } return null; @@ -141,11 +135,10 @@ class ObjectAccess * * @return mixed Value of the property */ - public static function getPropertyPath($subject, $propertyPath) + public static function getPropertyPath($subject, string $propertyPath) { - $propertyPathSegments = explode('.', $propertyPath); try { - foreach ($propertyPathSegments as $pathSegment) { + foreach (new PropertyPath($propertyPath) as $pathSegment) { $subject = self::getPropertyInternal($subject, $pathSegment); } } catch (Exception\PropertyNotAccessibleException $error) { @@ -173,8 +166,12 @@ class ObjectAccess * @throws \InvalidArgumentException in case $object was not an object or $propertyName was not a string * @return bool TRUE if the property could be set, FALSE otherwise */ - public static function setProperty(&$subject, $propertyName, $propertyValue, $forceDirectAccess = false) + public static function setProperty(&$subject, string $propertyName, $propertyValue, bool $forceDirectAccess = false): bool { + if ($forceDirectAccess === true) { + trigger_error('Argument $forceDirectAccess will be removed in TYPO3 11.0', E_USER_DEPRECATED); + } + if (is_array($subject) || ($subject instanceof \ArrayAccess && !$forceDirectAccess)) { $subject[$propertyName] = $propertyValue; return true; @@ -182,10 +179,13 @@ class ObjectAccess if (!is_object($subject)) { throw new \InvalidArgumentException('subject must be an object or array, ' . gettype($subject) . ' given.', 1237301368); } - if (!is_string($propertyName)) { - throw new \InvalidArgumentException('Given property name is not of type string.', 1231178878); + + $accessor = self::createAccessor(); + if ($accessor->isWritable($subject, $propertyName)) { + $accessor->setValue($subject, $propertyName, $propertyValue); + return true; } - $result = true; + if ($forceDirectAccess) { if (property_exists($subject, $propertyName)) { $propertyReflection = new \ReflectionProperty($subject, $propertyName); @@ -194,22 +194,11 @@ class ObjectAccess } else { $subject->{$propertyName} = $propertyValue; } - return $result; - } - $setterMethodName = self::buildSetterMethodName($propertyName); - if (is_callable([$subject, $setterMethodName])) { - $subject->{$setterMethodName}($propertyValue); - } elseif (property_exists($subject, $propertyName)) { - $reflection = new \ReflectionProperty($subject, $propertyName); - if ($reflection->isPublic()) { - $subject->{$propertyName} = $propertyValue; - } else { - $result = false; - } - } else { - $result = false; + + return true; } - return $result; + + return false; } /** @@ -221,51 +210,55 @@ class ObjectAccess * * @param object $object Object to receive property names for * - * @throws \InvalidArgumentException * @return array Array of all gettable property names + * @throws Exception\UnknownClassException */ - public static function getGettablePropertyNames($object) + public static function getGettablePropertyNames(object $object): array { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1237301369); - } if ($object instanceof \stdClass) { $properties = array_keys((array)$object); sort($properties); return $properties; } - $reflection = new \ReflectionClass($object); - $declaredPropertyNames = array_map( - function (\ReflectionProperty $property) { - return $property->getName(); - }, - $reflection->getProperties(\ReflectionProperty::IS_PUBLIC) - ); - foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { - $methodParameters = $method->getParameters(); - if (!empty($methodParameters)) { - foreach ($methodParameters as $parameter) { - if (!$parameter->isOptional()) { - continue 2; - } + $classSchema = GeneralUtility::makeInstance(ReflectionService::class) + ->getClassSchema($object); + + $accessor = self::createAccessor(); + $propertyNames = array_keys($classSchema->getProperties()); + $accessiblePropertyNames = array_filter($propertyNames, function ($propertyName) use ($accessor, $object) { + return $accessor->isReadable($object, $propertyName); + }); + + foreach ($classSchema->getMethods() as $methodName => $methodDefinition) { + if (!$methodDefinition->isPublic()) { + continue; + } + + foreach ($methodDefinition->getParameters() as $methodParam) { + if (!$methodParam->isOptional()) { + continue 2; } } - $methodName = $method->getName(); - if (strpos($methodName, 'is') === 0) { - $declaredPropertyNames[] = lcfirst(substr($methodName, 2)); + + if (StringUtility::beginsWith($methodName, 'get')) { + $accessiblePropertyNames[] = lcfirst(substr($methodName, 3)); + continue; } - if (strpos($methodName, 'get') === 0) { - $declaredPropertyNames[] = lcfirst(substr($methodName, 3)); + + if (StringUtility::beginsWith($methodName, 'has')) { + $accessiblePropertyNames[] = lcfirst(substr($methodName, 3)); + continue; } - if (strpos($methodName, 'has') === 0) { - $declaredPropertyNames[] = lcfirst(substr($methodName, 3)); + + if (StringUtility::beginsWith($methodName, 'is')) { + $accessiblePropertyNames[] = lcfirst(substr($methodName, 2)); } } - $propertyNames = array_unique($declaredPropertyNames); - sort($propertyNames); - return $propertyNames; + $accessiblePropertyNames = array_unique($accessiblePropertyNames); + sort($accessiblePropertyNames); + return $accessiblePropertyNames; } /** @@ -280,22 +273,29 @@ class ObjectAccess * @throws \InvalidArgumentException * @return array Array of all settable property names */ - public static function getSettablePropertyNames($object) + public static function getSettablePropertyNames(object $object): array { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1264022994); - } - if ($object instanceof \stdClass) { - $declaredPropertyNames = array_keys((array)$object); + $accessor = self::createAccessor(); + + if ($object instanceof \stdClass || $object instanceof \ArrayAccess) { + $propertyNames = array_keys((array)$object); } else { - $declaredPropertyNames = array_keys(get_class_vars(get_class($object))); - } - foreach (get_class_methods($object) as $methodName) { - if (strpos($methodName, 'set') === 0 && is_callable([$object, $methodName])) { - $declaredPropertyNames[] = lcfirst(substr($methodName, 3)); + $classSchema = GeneralUtility::makeInstance(ReflectionService::class)->getClassSchema($object); + + $propertyNames = array_filter(array_keys($classSchema->getProperties()), function ($methodName) use ($accessor, $object) { + return $accessor->isWritable($object, $methodName); + }); + + $setters = array_filter(array_keys($classSchema->getMethods()), function ($methodName) use ($object) { + return StringUtility::beginsWith($methodName, 'set') && is_callable([$object, $methodName]); + }); + + foreach ($setters as $setter) { + $propertyNames[] = lcfirst(substr($setter, 3)); } } - $propertyNames = array_unique($declaredPropertyNames); + + $propertyNames = array_unique($propertyNames); sort($propertyNames); return $propertyNames; } @@ -303,24 +303,21 @@ class ObjectAccess /** * Tells if the value of the specified property can be set by this Object Accessor. * - * @param object $object Object containting the property + * @param object $object Object containing the property * @param string $propertyName Name of the property to check * * @throws \InvalidArgumentException * @return bool */ - public static function isPropertySettable($object, $propertyName) + public static function isPropertySettable(object $object, $propertyName): bool { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1259828920); - } if ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) { return true; } if (array_key_exists($propertyName, get_class_vars(get_class($object)))) { return true; } - return is_callable([$object, self::buildSetterMethodName($propertyName)]); + return is_callable([$object, 'set' . ucfirst($propertyName)]); } /** @@ -332,31 +329,17 @@ class ObjectAccess * @throws \InvalidArgumentException * @return bool */ - public static function isPropertyGettable($object, $propertyName) + public static function isPropertyGettable($object, $propertyName): bool { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1259828921); - } - if ($object instanceof \ArrayAccess && isset($object[$propertyName])) { - return true; - } - if ($object instanceof \stdClass && isset($object->$propertyName)) { - return true; + if (($object instanceof \ArrayAccess) && !$object->offsetExists($propertyName)) { + return false; } - if (is_callable([$object, 'get' . ucfirst($propertyName)])) { - return true; - } - if (is_callable([$object, 'has' . ucfirst($propertyName)])) { - return true; - } - if (is_callable([$object, 'is' . ucfirst($propertyName)])) { - return true; - } - if (property_exists($object, $propertyName)) { - $propertyReflection = new \ReflectionProperty($object, $propertyName); - return $propertyReflection->isPublic(); + + if (is_array($object) || $object instanceof \ArrayAccess) { + $propertyName = self::wrap($propertyName); } - return false; + + return self::createAccessor()->isReadable($object, $propertyName); } /** @@ -369,11 +352,8 @@ class ObjectAccess * @return array Associative array of all properties. * @todo What to do with ArrayAccess */ - public static function getGettableProperties($object) + public static function getGettableProperties(object $object): array { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1237301370); - } $properties = []; foreach (self::getGettablePropertyNames($object) as $propertyName) { $properties[$propertyName] = self::getPropertyInternal($object, $propertyName); @@ -388,9 +368,88 @@ class ObjectAccess * @param string $propertyName Name of the property * * @return string Name of the setter method name + * @deprecated */ - public static function buildSetterMethodName($propertyName) + public static function buildSetterMethodName($propertyName): string { + trigger_error(__METHOD__ . ' will be removed in TYPO3 11.0', E_USER_DEPRECATED); + return 'set' . ucfirst($propertyName); } + + /** + * @return PropertyAccessor + */ + private static function createAccessor(): PropertyAccessor + { + if (static::$propertyAccessor === null) { + static::$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->getPropertyAccessor(); + } + + return static::$propertyAccessor; + } + + /** + * @param object $subject + * @param PropertyPath $propertyPath + * @param bool $forceDirectAccess + * @return mixed + * @throws Exception\PropertyNotAccessibleException + * @throws \ReflectionException + */ + private static function getObjectPropertyValue(object $subject, PropertyPath $propertyPath, bool $forceDirectAccess) + { + $accessor = self::createAccessor(); + + if ($accessor->isReadable($subject, $propertyPath)) { + return $accessor->getValue($subject, $propertyPath); + } + + $propertyName = (string)$propertyPath; + + if (!$forceDirectAccess) { + throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1476109666); + } + + if (!property_exists($subject, $propertyName)) { + throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1302855001); + } + + $propertyReflection = new \ReflectionProperty($subject, $propertyName); + $propertyReflection->setAccessible(true); + return $propertyReflection->getValue($subject); + } + + /** + * @param array $subject + * @param PropertyPath $propertyPath + * @return mixed + */ + private static function getArrayIndexValue(array $subject, PropertyPath $propertyPath) + { + return self::createAccessor()->getValue($subject, $propertyPath); + } + + /** + * @param PropertyPath $propertyPath + * @return PropertyPath + */ + private static function convertToArrayPropertyPath(PropertyPath $propertyPath): PropertyPath + { + $segments = array_map(function ($segment) { + return static::wrap($segment); + }, $propertyPath->getElements()); + + return new PropertyPath(implode('.', $segments)); + } + + /** + * @param string $segment + * @return string + */ + private static function wrap(string $segment): string + { + return '[' . $segment . ']'; + } } diff --git a/typo3/sysext/extbase/Classes/Validation/Validator/GenericObjectValidator.php b/typo3/sysext/extbase/Classes/Validation/Validator/GenericObjectValidator.php index def5f15fbe4e0c7717c5202bf374974bc774f4f5..2448c4933a945428a98d225ebb05ce063469b207 100644 --- a/typo3/sysext/extbase/Classes/Validation/Validator/GenericObjectValidator.php +++ b/typo3/sysext/extbase/Classes/Validation/Validator/GenericObjectValidator.php @@ -67,7 +67,15 @@ class GenericObjectValidator extends AbstractValidator implements ObjectValidato if (ObjectAccess::isPropertyGettable($object, $propertyName)) { return ObjectAccess::getProperty($object, $propertyName); } - return ObjectAccess::getProperty($object, $propertyName, true); + throw new \RuntimeException( + sprintf( + 'Could not get value of property "%s::%s", make sure the property is either public or has a getter get%3$s(), a hasser has%3$s() or an isser is%3$s().', + get_class($object), + $propertyName, + ucfirst($propertyName) + ), + 1546632293 + ); } /** diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php index 90b30f3808c4143d7f6bb5b2d9928f669ec855e9..fdd03e36fc43e5aea2b175e36214a142d2bf0cfa 100644 --- a/typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php @@ -27,6 +27,8 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase; */ class JsonViewTest extends UnitTestCase { + protected $resetSingletonInstances = true; + /** * @var \TYPO3\CMS\Extbase\Mvc\View\JsonView */ diff --git a/typo3/sysext/extbase/Tests/Unit/Reflection/ObjectAccessTest.php b/typo3/sysext/extbase/Tests/Unit/Reflection/ObjectAccessTest.php index ddcb864978c2aa3056aea8891bdf038390cfde1c..3dab3be2ec60ddc84a1d1e229bec587136b42cfb 100644 --- a/typo3/sysext/extbase/Tests/Unit/Reflection/ObjectAccessTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Reflection/ObjectAccessTest.php @@ -24,6 +24,11 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase; */ class ObjectAccessTest extends UnitTestCase { + /** + * @var bool Reset singletons created by subject + */ + protected $resetSingletonInstances = true; + /** * @var DummyClassWithGettersAndSetters */ @@ -58,35 +63,6 @@ class ObjectAccessTest extends UnitTestCase $this->assertEquals($property, 42, 'A property of a given object was not returned correctly.'); } - /** - * @test - */ - public function getPropertyReturnsExpectedValueForUnexposedPropertyIfForceDirectAccessIsTrue() - { - $property = ObjectAccess::getProperty($this->dummyObject, 'unexposedProperty', true); - $this->assertEquals($property, 'unexposed', 'A property of a given object was not returned correctly.'); - } - - /** - * @test - */ - public function getPropertyReturnsExpectedValueForUnknownPropertyIfForceDirectAccessIsTrue() - { - $this->dummyObject->unknownProperty = 'unknown'; - $property = ObjectAccess::getProperty($this->dummyObject, 'unknownProperty', true); - $this->assertEquals($property, 'unknown', 'A property of a given object was not returned correctly.'); - } - - /** - * @test - */ - public function getPropertyThrowsPropertyNotAccessibleExceptionForNotExistingPropertyIfForceDirectAccessIsTrue() - { - $this->expectException(PropertyNotAccessibleException::class); - $this->expectExceptionCode(1302855001); - ObjectAccess::getProperty($this->dummyObject, 'notExistingProperty', true); - } - /** * @test */ @@ -115,26 +91,6 @@ class ObjectAccessTest extends UnitTestCase $this->assertTrue($property); } - /** - * @test - */ - public function getPropertyThrowsExceptionIfThePropertyNameIsNotAString() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionCode(1231178303); - ObjectAccess::getProperty($this->dummyObject, new \ArrayObject()); - } - - /** - * @test - */ - public function setPropertyThrowsExceptionIfThePropertyNameIsNotAString() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionCode(1231178878); - ObjectAccess::setProperty($this->dummyObject, new \ArrayObject(), 42); - } - /** * @test */ @@ -143,24 +99,6 @@ class ObjectAccessTest extends UnitTestCase $this->assertFalse(ObjectAccess::setProperty($this->dummyObject, 'protectedProperty', 42)); } - /** - * @test - */ - public function setPropertySetsValueIfPropertyIsNotAccessibleWhenForceDirectAccessIsTrue() - { - $this->assertTrue(ObjectAccess::setProperty($this->dummyObject, 'unexposedProperty', 'was set anyway', true)); - $this->assertAttributeEquals('was set anyway', 'unexposedProperty', $this->dummyObject); - } - - /** - * @test - */ - public function setPropertySetsValueIfPropertyDoesNotExistWhenForceDirectAccessIsTrue() - { - $this->assertTrue(ObjectAccess::setProperty($this->dummyObject, 'unknownProperty', 'was set anyway', true)); - $this->assertAttributeEquals('was set anyway', 'unknownProperty', $this->dummyObject); - } - /** * @test */ diff --git a/typo3/sysext/extbase/Tests/Unit/Validation/Validator/CollectionValidatorTest.php b/typo3/sysext/extbase/Tests/Unit/Validation/Validator/CollectionValidatorTest.php index fd3da88626451fb1b3ade630e0172d347e698154..9c1a38ca7c9d9251c21da2e4640e573e4ea7afa5 100644 --- a/typo3/sysext/extbase/Tests/Unit/Validation/Validator/CollectionValidatorTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Validation/Validator/CollectionValidatorTest.php @@ -166,7 +166,6 @@ class CollectionValidatorTest extends UnitTestCase 'someProperty', ['someNotEmptyValue'] ); - \TYPO3\CMS\Extbase\Reflection\ObjectAccess::setProperty($lazyObjectStorage, 'isInitialized', false, true); // only in this test case we want to mock the isValid method $validator = $this->getValidator(['elementType' => $elementType], ['isValid']); $validator->expects($this->never())->method('isValid'); diff --git a/typo3/sysext/extbase/Tests/Unit/Validation/Validator/GenericObjectValidatorTest.php b/typo3/sysext/extbase/Tests/Unit/Validation/Validator/GenericObjectValidatorTest.php index 6b39c92b8943917bf98f78f92f7b1e24f0cb1c8d..c76e450e97a1a0bf06fd31e15cb7939b5c39ea36 100644 --- a/typo3/sysext/extbase/Tests/Unit/Validation/Validator/GenericObjectValidatorTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Validation/Validator/GenericObjectValidatorTest.php @@ -58,6 +58,16 @@ class GenericObjectValidatorTest extends UnitTestCase $objectWithPrivateProperties = new class() { protected $foo = 'foovalue'; protected $bar = 'barvalue'; + + public function getFoo() + { + return $this->foo; + } + + public function getBar() + { + return $this->bar; + } }; return [ diff --git a/typo3/sysext/extbase/Tests/UnitDeprecated/Reflection/ObjectAccessTest.php b/typo3/sysext/extbase/Tests/UnitDeprecated/Reflection/ObjectAccessTest.php new file mode 100644 index 0000000000000000000000000000000000000000..948257d04fc860567b2c3115deea06ae403dd552 --- /dev/null +++ b/typo3/sysext/extbase/Tests/UnitDeprecated/Reflection/ObjectAccessTest.php @@ -0,0 +1,95 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Extbase\Tests\UnitDeprecated\Reflection; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +use TYPO3\CMS\Extbase\Reflection\Exception\PropertyNotAccessibleException; +use TYPO3\CMS\Extbase\Reflection\ObjectAccess; +use TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture\DummyClassWithGettersAndSetters; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * Test case + */ +class ObjectAccessTest extends UnitTestCase +{ + /** + * @var bool Reset singletons created by subject + */ + protected $resetSingletonInstances = true; + + /** + * @var DummyClassWithGettersAndSetters + */ + protected $dummyObject; + + /** + * Set up + */ + protected function setUp() + { + $this->dummyObject = new DummyClassWithGettersAndSetters(); + $this->dummyObject->setProperty('string1'); + $this->dummyObject->setAnotherProperty(42); + $this->dummyObject->shouldNotBePickedUp = true; + } + + /** + * @test + */ + public function getPropertyReturnsExpectedValueForUnexposedPropertyIfForceDirectAccessIsTrue() + { + $property = ObjectAccess::getProperty($this->dummyObject, 'unexposedProperty', true); + $this->assertEquals($property, 'unexposed', 'A property of a given object was not returned correctly.'); + } + + /** + * @test + */ + public function getPropertyReturnsExpectedValueForUnknownPropertyIfForceDirectAccessIsTrue() + { + $this->dummyObject->unknownProperty = 'unknown'; + $property = ObjectAccess::getProperty($this->dummyObject, 'unknownProperty', true); + $this->assertEquals($property, 'unknown', 'A property of a given object was not returned correctly.'); + } + + /** + * @test + */ + public function getPropertyThrowsPropertyNotAccessibleExceptionForNotExistingPropertyIfForceDirectAccessIsTrue() + { + $this->expectException(PropertyNotAccessibleException::class); + $this->expectExceptionCode(1302855001); + ObjectAccess::getProperty($this->dummyObject, 'notExistingProperty', true); + } + + /** + * @test + */ + public function setPropertySetsValueIfPropertyIsNotAccessibleWhenForceDirectAccessIsTrue() + { + $this->assertTrue(ObjectAccess::setProperty($this->dummyObject, 'unexposedProperty', 'was set anyway', true)); + $this->assertAttributeEquals('was set anyway', 'unexposedProperty', $this->dummyObject); + } + + /** + * @test + */ + public function setPropertySetsValueIfPropertyDoesNotExistWhenForceDirectAccessIsTrue() + { + $this->assertTrue(ObjectAccess::setProperty($this->dummyObject, 'unknownProperty', 'was set anyway', true)); + $this->assertAttributeEquals('was set anyway', 'unknownProperty', $this->dummyObject); + } +} diff --git a/typo3/sysext/extbase/composer.json b/typo3/sysext/extbase/composer.json index c00971ef457a69a97a716757116743512728b1e6..c8a7842b2a780bbf6a2d7766fd4657ae7b5bdf57 100644 --- a/typo3/sysext/extbase/composer.json +++ b/typo3/sysext/extbase/composer.json @@ -14,6 +14,7 @@ }, "require": { "phpdocumentor/reflection-docblock": "^4.3", + "symfony/property-access": "^4.2", "symfony/property-info": "^4.2", "typo3/cms-core": "10.0.*@dev", "webmozart/assert": "^1.0" diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedStaticMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedStaticMatcher.php index 79533650c2de356d128aab583b0b4d31344e7a49..950dffcce78c93c5efccc8160bd00809b97a0a3c 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedStaticMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedStaticMatcher.php @@ -47,4 +47,22 @@ return [ 'Deprecation-85801-GeneralUtilityexplodeUrl2Array-2ndMethodArgument.rst', ], ], + 'TYPO3\CMS\Extbase\Reflection\ObjectAccess::getProperty' => [ + 'maximumNumberOfArguments' => 2, + 'restFiles' => [ + 'Deprecation-87332-AvoidRuntimeReflectionCallsInObjectAccess.rst', + ], + ], + 'TYPO3\CMS\Extbase\Reflection\ObjectAccess::getPropertyInternal' => [ + 'maximumNumberOfArguments' => 2, + 'restFiles' => [ + 'Deprecation-87332-AvoidRuntimeReflectionCallsInObjectAccess.rst', + ], + ], + 'TYPO3\CMS\Extbase\Reflection\ObjectAccess::setProperty' => [ + 'maximumNumberOfArguments' => 3, + 'restFiles' => [ + 'Deprecation-87332-AvoidRuntimeReflectionCallsInObjectAccess.rst', + ], + ], ]; diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php index fb54759300bd6d53d3a110412c3a1f7900e3de73..5c98003cc7493a926deadad57449a241758fb9da 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php @@ -854,4 +854,11 @@ return [ 'Deprecation-87550-UseControllerClassesWhenRegisteringPluginsmodules.rst', ], ], + 'TYPO3\CMS\Extbase\Reflection\ObjectAccess::buildSetterMethodName' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 1, + 'restFiles' => [ + 'Deprecation-87332-AvoidRuntimeReflectionCallsInObjectAccess.rst', + ], + ], ];