isReadable($subject, $propertyPath)) { return $accessor->getValue($subject, $propertyPath); } // 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); } try { return self::getArrayIndexValue($subject, self::convertToArrayPropertyPath($propertyPath)); } catch (NoSuchIndexException) { return null; } } /** * Gets a property path from a given object or array. * * If propertyPath is "bla.blubb", then we first call getProperty($object, 'bla'), * and on the resulting object we call getProperty(..., 'blubb') * * For arrays the keys are checked likewise. * * @param object|array $subject Object or array to get the property path from * @param string $propertyPath * * @return mixed Value of the property */ public static function getPropertyPath(object|array $subject, string $propertyPath): mixed { try { foreach (new PropertyPath($propertyPath) as $pathSegment) { $subject = self::getPropertyInternal($subject, $pathSegment); } } catch (\TypeError|PropertyNotAccessibleException) { return null; } return $subject; } /** * Set a property for a given object. * Tries to set the property the following ways: * - if target is an array, set value * - if super cow powers should be used, set value through reflection * - if public setter method exists, call it. * - if public property exists, set it directly. * - if the target object is an instance of ArrayAccess, it sets the property * on it without checking if it existed. * - else, return FALSE * * @param object|array $subject The target object or array * @param string $propertyName Name of the property to set * @param mixed $propertyValue Value of the property * * @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(object|array &$subject, string $propertyName, mixed $propertyValue): bool { if (is_array($subject) || $subject instanceof \ArrayAccess) { $subject[$propertyName] = $propertyValue; return true; } $accessor = self::createAccessor(); if ($accessor->isWritable($subject, $propertyName)) { $accessor->setValue($subject, $propertyName, $propertyValue); return true; } return false; } /** * Returns an array of properties which can be get with the getProperty() * method. * Includes the following properties: * - which can be get through a public getter method. * - public properties which can be directly get. * * @param object $object Object to receive property names for * * @return list Array of all gettable property names * @throws Exception\UnknownClassException */ public static function getGettablePropertyNames(object $object): array { if ($object instanceof \stdClass) { $properties = array_keys((array)$object); sort($properties); return $properties; } $classSchema = GeneralUtility::makeInstance(ReflectionService::class) ->getClassSchema($object); $accessiblePropertyNames = []; foreach ($classSchema->getProperties() as $propertyName => $propertyDefinition) { if ($propertyDefinition->isPublic()) { $accessiblePropertyNames[] = $propertyName; continue; } $accessors = [ 'get' . ucfirst($propertyName), 'has' . ucfirst($propertyName), 'is' . ucfirst($propertyName), ]; foreach ($accessors as $accessor) { if (!$classSchema->hasMethod($accessor)) { continue; } if (!$classSchema->getMethod($accessor)->isPublic()) { continue; } foreach ($classSchema->getMethod($accessor)->getParameters() as $methodParam) { if (!$methodParam->isOptional()) { continue 2; } } if (!is_callable([$object, $accessor])) { continue; } $accessiblePropertyNames[] = $propertyName; } } // Fallback mechanism to not break former behaviour // // todo: Checking accessor methods of virtual(non-existing) properties should be removed (breaking) in // upcoming versions. It was an unintentionally added "feature" in the past. It contradicts the method // name "getGettablePropertyNames". foreach ($classSchema->getMethods() as $methodName => $methodDefinition) { $propertyName = null; if (str_starts_with($methodName, 'get') || str_starts_with($methodName, 'has')) { $propertyName = lcfirst(substr($methodName, 3)); } if (str_starts_with($methodName, 'is')) { $propertyName = lcfirst(substr($methodName, 2)); } if ($propertyName === null) { continue; } if (!$methodDefinition->isPublic()) { continue; } foreach ($methodDefinition->getParameters() as $methodParam) { if (!$methodParam->isOptional()) { continue 2; } } $accessiblePropertyNames[] = $propertyName; } $accessiblePropertyNames = array_unique($accessiblePropertyNames); sort($accessiblePropertyNames); return $accessiblePropertyNames; } /** * Returns an array of properties which can be set with the setProperty() * method. * Includes the following properties: * - which can be set through a public setter method. * - public properties which can be directly set. * * @param object $object Object to receive property names for * * @throws \InvalidArgumentException * @return list Array of all settable property names */ public static function getSettablePropertyNames(object $object): array { $accessor = self::createAccessor(); if ($object instanceof \stdClass || $object instanceof \ArrayAccess) { $propertyNames = array_keys((array)$object); } else { $classSchema = GeneralUtility::makeInstance(ReflectionService::class)->getClassSchema($object); $propertyNames = array_filter( array_keys($classSchema->getProperties()), static fn(string $propertyName): bool => $accessor->isWritable($object, $propertyName) ); $setters = array_filter( array_keys($classSchema->getMethods()), static fn(string $methodName): bool => str_starts_with($methodName, 'set') && is_callable([$object, $methodName]) ); foreach ($setters as $setter) { $propertyNames[] = lcfirst(substr($setter, 3)); } } $propertyNames = array_unique($propertyNames); sort($propertyNames); return $propertyNames; } /** * Tells if the value of the specified property can be set by this Object Accessor. * * @param object $object Object containing the property * @param string $propertyName Name of the property to check */ public static function isPropertySettable(object $object, string $propertyName): bool { 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, 'set' . ucfirst($propertyName)]); } /** * Tells if the value of the specified property can be retrieved by this Object Accessor. * * @param object|array $object Object containing the property * @param string $propertyName Name of the property to check * * @throws \InvalidArgumentException */ public static function isPropertyGettable(object|array $object, string $propertyName): bool { if (is_array($object) || ($object instanceof \ArrayAccess && $object->offsetExists($propertyName))) { $propertyName = self::wrap($propertyName); } return self::createAccessor()->isReadable($object, $propertyName); } /** * Get all properties (names and their current values) of the current * $object that are accessible through this class. * * @param object $object Object to get all properties from. * * @throws \InvalidArgumentException * @return array Associative array of all properties. * @todo What to do with ArrayAccess */ public static function getGettableProperties(object $object): array { $properties = []; foreach (self::getGettablePropertyNames($object) as $propertyName) { $properties[$propertyName] = self::getPropertyInternal($object, $propertyName); } return $properties; } private static function createAccessor(): PropertyAccessorInterface { if (self::$propertyAccessor === null) { self::$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->enableExceptionOnInvalidIndex() ->getPropertyAccessor(); } return self::$propertyAccessor; } /** * @throws Exception\PropertyNotAccessibleException */ private static function getObjectPropertyValue(object $subject, PropertyPath $propertyPath): mixed { $accessor = self::createAccessor(); if ($accessor->isReadable($subject, $propertyPath)) { return $accessor->getValue($subject, $propertyPath); } throw new PropertyNotAccessibleException('The property "' . (string)$propertyPath . '" on the subject does not exist.', 1476109666); } private static function getArrayIndexValue(array $subject, PropertyPath $propertyPath): mixed { return self::createAccessor()->getValue($subject, $propertyPath); } private static function convertToArrayPropertyPath(PropertyPath $propertyPath): PropertyPath { $segments = array_map(static fn(string $segment): string => self::wrap($segment), $propertyPath->getElements()); return new PropertyPath(implode('.', $segments)); } private static function wrap(string $segment): string { return '[' . $segment . ']'; } }