‪TYPO3CMS  ‪main
ObjectAccess.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
21 use Symfony\Component\PropertyAccess\PropertyAccess;
22 use Symfony\Component\PropertyAccess\PropertyAccessor;
23 use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
24 use Symfony\Component\PropertyAccess\PropertyPath;
28 
39 {
40  private static PropertyAccessorInterface|null ‪$propertyAccessor = null;
41 
59  public static function ‪getProperty(object|array $subject, string $propertyName): mixed
60  {
61  try {
62  return ‪self::getPropertyInternal($subject, $propertyName);
63  } catch (NoSuchIndexException) {
64  return null;
65  }
66  }
67 
82  public static function ‪getPropertyInternal(object|array $subject, string $propertyName): mixed
83  {
84  if ($subject instanceof \SplObjectStorage || $subject instanceof ‪ObjectStorage) {
85  $subject = iterator_to_array(clone $subject, false);
86  }
87 
88  $propertyPath = new PropertyPath($propertyName);
89 
90  if ($subject instanceof \ArrayAccess) {
91  $accessor = self::createAccessor();
92 
93  // Check if $subject is an instance of \ArrayAccess and therefore maybe has actual accessible properties.
94  if ($accessor->isReadable($subject, $propertyPath)) {
95  return $accessor->getValue($subject, $propertyPath);
96  }
97 
98  // Use array style property path for instances of \ArrayAccess
99  // https://symfony.com/doc/current/components/property_access.html#reading-from-arrays
100 
101  $propertyPath = ‪self::convertToArrayPropertyPath($propertyPath);
102  }
103 
104  if (is_object($subject)) {
105  return ‪self::getObjectPropertyValue($subject, $propertyPath);
106  }
107 
108  try {
109  return ‪self::getArrayIndexValue($subject, self::convertToArrayPropertyPath($propertyPath));
110  } catch (NoSuchIndexException) {
111  return null;
112  }
113  }
114 
128  public static function ‪getPropertyPath(object|array $subject, string $propertyPath): mixed
129  {
130  try {
131  foreach (new PropertyPath($propertyPath) as $pathSegment) {
132  $subject = ‪self::getPropertyInternal($subject, $pathSegment);
133  }
134  } catch (\TypeError|‪PropertyNotAccessibleException) {
135  return null;
136  }
137  return $subject;
138  }
139 
158  public static function ‪setProperty(object|array &$subject, string $propertyName, mixed $propertyValue): bool
159  {
160  if (is_array($subject) || $subject instanceof \ArrayAccess) {
161  $subject[$propertyName] = $propertyValue;
162  return true;
163  }
164 
165  $accessor = self::createAccessor();
166  if ($accessor->isWritable($subject, $propertyName)) {
167  $accessor->setValue($subject, $propertyName, $propertyValue);
168  return true;
169  }
170  return false;
171  }
172 
185  public static function ‪getGettablePropertyNames(object $object): array
186  {
187  if ($object instanceof \stdClass) {
188  $properties = array_keys((array)$object);
189  sort($properties);
190  return $properties;
191  }
192 
193  $classSchema = GeneralUtility::makeInstance(ReflectionService::class)
194  ->getClassSchema($object);
195 
196  $accessiblePropertyNames = [];
197  foreach ($classSchema->getProperties() as $propertyName => $propertyDefinition) {
198  if ($propertyDefinition->isPublic()) {
199  $accessiblePropertyNames[] = $propertyName;
200  continue;
201  }
202 
203  $accessors = [
204  'get' . ucfirst($propertyName),
205  'has' . ucfirst($propertyName),
206  'is' . ucfirst($propertyName),
207  ];
208 
209  foreach ($accessors as $accessor) {
210  if (!$classSchema->hasMethod($accessor)) {
211  continue;
212  }
213 
214  if (!$classSchema->getMethod($accessor)->isPublic()) {
215  continue;
216  }
217 
218  foreach ($classSchema->getMethod($accessor)->getParameters() as $methodParam) {
219  if (!$methodParam->isOptional()) {
220  continue 2;
221  }
222  }
223 
224  if (!is_callable([$object, $accessor])) {
225  continue;
226  }
227 
228  $accessiblePropertyNames[] = $propertyName;
229  }
230  }
231 
232  // Fallback mechanism to not break former behaviour
233  //
234  // todo: Checking accessor methods of virtual(non-existing) properties should be removed (breaking) in
235  // upcoming versions. It was an unintentionally added "feature" in the past. It contradicts the method
236  // name "getGettablePropertyNames".
237  foreach ($classSchema->getMethods() as $methodName => $methodDefinition) {
238  $propertyName = null;
239  if (str_starts_with($methodName, 'get') || str_starts_with($methodName, 'has')) {
240  $propertyName = lcfirst(substr($methodName, 3));
241  }
242 
243  if (str_starts_with($methodName, 'is')) {
244  $propertyName = lcfirst(substr($methodName, 2));
245  }
246 
247  if ($propertyName === null) {
248  continue;
249  }
250 
251  if (!$methodDefinition->isPublic()) {
252  continue;
253  }
254 
255  foreach ($methodDefinition->getParameters() as $methodParam) {
256  if (!$methodParam->isOptional()) {
257  continue 2;
258  }
259  }
260 
261  $accessiblePropertyNames[] = $propertyName;
262  }
263 
264  $accessiblePropertyNames = array_unique($accessiblePropertyNames);
265  sort($accessiblePropertyNames);
266  return $accessiblePropertyNames;
267  }
268 
281  public static function ‪getSettablePropertyNames(object $object): array
282  {
283  $accessor = self::createAccessor();
284 
285  if ($object instanceof \stdClass || $object instanceof \ArrayAccess) {
286  $propertyNames = array_keys((array)$object);
287  } else {
288  $classSchema = GeneralUtility::makeInstance(ReflectionService::class)->getClassSchema($object);
289 
290  $propertyNames = array_filter(
291  array_keys($classSchema->getProperties()),
292  static fn(string $propertyName): bool => $accessor->isWritable($object, $propertyName)
293  );
294 
295  $setters = array_filter(
296  array_keys($classSchema->getMethods()),
297  static fn(string $methodName): bool => str_starts_with($methodName, 'set') && is_callable([$object, $methodName])
298  );
299 
300  foreach ($setters as $setter) {
301  $propertyNames[] = lcfirst(substr($setter, 3));
302  }
303  }
304 
305  $propertyNames = array_unique($propertyNames);
306  sort($propertyNames);
307  return $propertyNames;
308  }
309 
316  public static function ‪isPropertySettable(object $object, string $propertyName): bool
317  {
318  if ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) {
319  return true;
320  }
321  if (array_key_exists($propertyName, get_class_vars(get_class($object)))) {
322  return true;
323  }
324  return is_callable([$object, 'set' . ucfirst($propertyName)]);
325  }
326 
335  public static function ‪isPropertyGettable(object|array $object, string $propertyName): bool
336  {
337  if (is_array($object) || ($object instanceof \ArrayAccess && $object->offsetExists($propertyName))) {
338  $propertyName = ‪self::wrap($propertyName);
339  }
340 
341  return self::createAccessor()->isReadable($object, $propertyName);
342  }
343 
354  public static function getGettableProperties(object $object): array
355  {
356  $properties = [];
357  foreach (self::getGettablePropertyNames($object) as $propertyName) {
358  $properties[$propertyName] = ‪self::getPropertyInternal($object, $propertyName);
359  }
360  return $properties;
361  }
362 
363  private static function createAccessor(): PropertyAccessor
364  {
365  if (self::$propertyAccessor === null) {
366  self::$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
367  ->enableExceptionOnInvalidIndex()
368  ->getPropertyAccessor();
369  }
370 
372  }
373 
377  private static function ‪getObjectPropertyValue(object $subject, PropertyPath $propertyPath): mixed
378  {
379  $accessor = self::createAccessor();
380 
381  if ($accessor->isReadable($subject, $propertyPath)) {
382  return $accessor->getValue($subject, $propertyPath);
383  }
384 
385  throw new ‪PropertyNotAccessibleException('The property "' . (string)$propertyPath . '" on the subject does not exist.', 1476109666);
386  }
387 
388  private static function ‪getArrayIndexValue(array $subject, PropertyPath $propertyPath): mixed
389  {
390  return self::createAccessor()->getValue($subject, $propertyPath);
391  }
392 
393  private static function ‪convertToArrayPropertyPath(PropertyPath $propertyPath): PropertyPath
394  {
395  $segments = array_map(static fn(string $segment): string => static::wrap($segment), $propertyPath->getElements());
396 
397  return new PropertyPath(implode('.', $segments));
398  }
399 
400  private static function ‪wrap(string $segment): string
401  {
402  return '[' . $segment . ']';
403  }
404 }
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getPropertyInternal
‪static mixed getPropertyInternal(object|array $subject, string $propertyName)
Definition: ObjectAccess.php:82
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getPropertyPath
‪static mixed getPropertyPath(object|array $subject, string $propertyPath)
Definition: ObjectAccess.php:128
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getProperty
‪static mixed getProperty(object|array $subject, string $propertyName)
Definition: ObjectAccess.php:59
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\wrap
‪static wrap(string $segment)
Definition: ObjectAccess.php:400
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getObjectPropertyValue
‪static getObjectPropertyValue(object $subject, PropertyPath $propertyPath)
Definition: ObjectAccess.php:377
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\setProperty
‪static bool setProperty(object|array &$subject, string $propertyName, mixed $propertyValue)
Definition: ObjectAccess.php:158
‪TYPO3\CMS\Extbase\Reflection\Exception\PropertyNotAccessibleException
Definition: PropertyNotAccessibleException.php:25
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\isPropertySettable
‪static isPropertySettable(object $object, string $propertyName)
Definition: ObjectAccess.php:316
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess
Definition: ObjectAccess.php:39
‪TYPO3\CMS\Extbase\Persistence\ObjectStorage
Definition: ObjectStorage.php:34
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\convertToArrayPropertyPath
‪static convertToArrayPropertyPath(PropertyPath $propertyPath)
Definition: ObjectAccess.php:393
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\$propertyAccessor
‪static PropertyAccessorInterface null $propertyAccessor
Definition: ObjectAccess.php:40
‪TYPO3\CMS\Extbase\Reflection
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getGettablePropertyNames
‪static list< string > getGettablePropertyNames(object $object)
Definition: ObjectAccess.php:185
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\isPropertyGettable
‪static isPropertyGettable(object|array $object, string $propertyName)
Definition: ObjectAccess.php:335
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getArrayIndexValue
‪static getArrayIndexValue(array $subject, PropertyPath $propertyPath)
Definition: ObjectAccess.php:388
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getSettablePropertyNames
‪static list< string > getSettablePropertyNames(object $object)
Definition: ObjectAccess.php:281