‪TYPO3CMS  11.5
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 {
43  private static ‪$propertyAccessor;
44 
62  public static function ‪getProperty($subject, string $propertyName)
63  {
64  if (!is_object($subject) && !is_array($subject)) {
65  throw new \InvalidArgumentException(
66  '$subject must be an object or array, ' . gettype($subject) . ' given.',
67  1237301367
68  );
69  }
70  try {
71  return ‪self::getPropertyInternal($subject, $propertyName);
72  } catch (NoSuchIndexException $ignoredException) {
73  return null;
74  }
75  }
76 
91  public static function ‪getPropertyInternal($subject, string $propertyName)
92  {
93  if ($subject instanceof \SplObjectStorage || $subject instanceof ‪ObjectStorage) {
94  $subject = iterator_to_array(clone $subject, false);
95  }
96 
97  $propertyPath = new PropertyPath($propertyName);
98 
99  if ($subject instanceof \ArrayAccess) {
100  $accessor = ‪self::createAccessor();
101 
102  // Check if $subject is an instance of \ArrayAccess and therefore maybe has actual accessible properties.
103  if ($accessor->isReadable($subject, $propertyPath)) {
104  return $accessor->getValue($subject, $propertyPath);
105  }
106 
107  // Use array style property path for instances of \ArrayAccess
108  // https://symfony.com/doc/current/components/property_access.html#reading-from-arrays
109 
110  $propertyPath = ‪self::convertToArrayPropertyPath($propertyPath);
111  }
112 
113  if (is_object($subject)) {
114  return ‪self::getObjectPropertyValue($subject, $propertyPath);
115  }
116 
117  if (is_array($subject)) {
118  try {
119  return ‪self::getArrayIndexValue($subject, self::convertToArrayPropertyPath($propertyPath));
120  } catch (NoSuchIndexException $ignoredException) {
121  return null;
122  }
123  }
124 
125  return null;
126  }
127 
141  public static function ‪getPropertyPath($subject, string $propertyPath)
142  {
143  try {
144  foreach (new PropertyPath($propertyPath) as $pathSegment) {
145  $subject = ‪self::getPropertyInternal($subject, $pathSegment);
146  }
147  } catch (‪PropertyNotAccessibleException $error) {
148  return null;
149  }
150  return $subject;
151  }
152 
171  public static function ‪setProperty(&$subject, string $propertyName, $propertyValue): bool
172  {
173  if (is_array($subject) || $subject instanceof \ArrayAccess) {
174  $subject[$propertyName] = $propertyValue;
175  return true;
176  }
177  if (!is_object($subject)) {
178  throw new \InvalidArgumentException('subject must be an object or array, ' . gettype($subject) . ' given.', 1237301368);
179  }
180 
181  $accessor = ‪self::createAccessor();
182  if ($accessor->isWritable($subject, $propertyName)) {
183  $accessor->setValue($subject, $propertyName, $propertyValue);
184  return true;
185  }
186  return false;
187  }
188 
201  public static function ‪getGettablePropertyNames(object $object): array
202  {
203  if ($object instanceof \stdClass) {
204  $properties = array_keys((array)$object);
205  sort($properties);
206  return $properties;
207  }
208 
209  $classSchema = GeneralUtility::makeInstance(ReflectionService::class)
210  ->getClassSchema($object);
211 
212  $accessiblePropertyNames = [];
213  foreach ($classSchema->getProperties() as $propertyName => $propertyDefinition) {
214  if ($propertyDefinition->isPublic()) {
215  $accessiblePropertyNames[] = $propertyName;
216  continue;
217  }
218 
219  $accessors = [
220  'get' . ucfirst($propertyName),
221  'has' . ucfirst($propertyName),
222  'is' . ucfirst($propertyName),
223  ];
224 
225  foreach ($accessors as $accessor) {
226  if (!$classSchema->hasMethod($accessor)) {
227  continue;
228  }
229 
230  if (!$classSchema->getMethod($accessor)->isPublic()) {
231  continue;
232  }
233 
234  foreach ($classSchema->getMethod($accessor)->getParameters() as $methodParam) {
235  if (!$methodParam->isOptional()) {
236  continue 2;
237  }
238  }
239 
240  if (!is_callable([$object, $accessor])) {
241  continue;
242  }
243 
244  $accessiblePropertyNames[] = $propertyName;
245  }
246  }
247 
248  // Fallback mechanism to not break former behaviour
249  //
250  // todo: Checking accessor methods of virtual(non-existing) properties should be removed (breaking) in
251  // upcoming versions. It was an unintentionally added "feature" in the past. It contradicts the method
252  // name "getGettablePropertyNames".
253  foreach ($classSchema->getMethods() as $methodName => $methodDefinition) {
254  $propertyName = null;
255  if (str_starts_with($methodName, 'get') || str_starts_with($methodName, 'has')) {
256  $propertyName = lcfirst(substr($methodName, 3));
257  }
258 
259  if (str_starts_with($methodName, 'is')) {
260  $propertyName = lcfirst(substr($methodName, 2));
261  }
262 
263  if ($propertyName === null) {
264  continue;
265  }
266 
267  if (!$methodDefinition->isPublic()) {
268  continue;
269  }
270 
271  foreach ($methodDefinition->getParameters() as $methodParam) {
272  if (!$methodParam->isOptional()) {
273  continue 2;
274  }
275  }
276 
277  $accessiblePropertyNames[] = $propertyName;
278  }
279 
280  $accessiblePropertyNames = array_unique($accessiblePropertyNames);
281  sort($accessiblePropertyNames);
282  return $accessiblePropertyNames;
283  }
284 
297  public static function ‪getSettablePropertyNames(object $object): array
298  {
299  $accessor = ‪self::createAccessor();
300 
301  if ($object instanceof \stdClass || $object instanceof \ArrayAccess) {
302  $propertyNames = array_keys((array)$object);
303  } else {
304  $classSchema = GeneralUtility::makeInstance(ReflectionService::class)->getClassSchema($object);
305 
306  $propertyNames = array_filter(array_keys($classSchema->getProperties()), static ‪function ($methodName) use ($accessor, $object) {
307  return $accessor->isWritable($object, $methodName);
308  });
309 
310  $setters = array_filter(array_keys($classSchema->getMethods()), static ‪function ($methodName) use ($object) {
311  return str_starts_with($methodName, 'set') && is_callable([$object, $methodName]);
312  });
313 
314  foreach ($setters as $setter) {
315  $propertyNames[] = lcfirst(substr($setter, 3));
316  }
317  }
318 
319  $propertyNames = array_unique($propertyNames);
320  sort($propertyNames);
321  return $propertyNames;
322  }
323 
333  public static function ‪isPropertySettable(object $object, $propertyName): bool
334  {
335  if ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) {
336  return true;
337  }
338  if (array_key_exists($propertyName, get_class_vars(get_class($object)))) {
339  return true;
340  }
341  return is_callable([$object, 'set' . ucfirst($propertyName)]);
342  }
343 
353  public static function ‪isPropertyGettable($object, $propertyName): bool
354  {
355  if (is_array($object) || ($object instanceof \ArrayAccess && $object->offsetExists($propertyName))) {
356  $propertyName = ‪self::wrap($propertyName);
357  }
358 
359  return ‪self::createAccessor()->isReadable($object, $propertyName);
360  }
361 
372  public static function ‪getGettableProperties(object $object): array
373  {
374  $properties = [];
375  foreach (self::getGettablePropertyNames($object) as $propertyName) {
376  $properties[$propertyName] = ‪self::getPropertyInternal($object, $propertyName);
377  }
378  return $properties;
379  }
380 
384  private static function ‪createAccessor(): PropertyAccessor
385  {
386  if (self::$propertyAccessor === null) {
387  self::$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
388  ->enableExceptionOnInvalidIndex()
389  ->getPropertyAccessor();
390  }
391 
393  }
394 
401  private static function ‪getObjectPropertyValue(object $subject, PropertyPath $propertyPath)
402  {
403  $accessor = ‪self::createAccessor();
404 
405  if ($accessor->isReadable($subject, $propertyPath)) {
406  return $accessor->getValue($subject, $propertyPath);
407  }
408 
409  throw new ‪PropertyNotAccessibleException('The property "' . (string)$propertyPath . '" on the subject does not exist.', 1476109666);
410  }
411 
417  private static function ‪getArrayIndexValue(array $subject, PropertyPath $propertyPath)
418  {
419  return ‪self::createAccessor()->getValue($subject, $propertyPath);
420  }
421 
426  private static function ‪convertToArrayPropertyPath(PropertyPath $propertyPath): PropertyPath
427  {
428  $segments = array_map(static function ($segment) {
429  return static::wrap($segment);
430  }, $propertyPath->getElements());
431 
432  return new PropertyPath(implode('.', $segments));
433  }
434 
439  private static function ‪wrap(string $segment): string
440  {
441  return '[' . $segment . ']';
442  }
443 }
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\convertToArrayPropertyPath
‪static PropertyPath convertToArrayPropertyPath(PropertyPath $propertyPath)
Definition: ObjectAccess.php:425
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\$propertyAccessor
‪static PropertyAccessorInterface $propertyAccessor
Definition: ObjectAccess.php:42
‪TYPO3\CMS\Extbase\function
‪static return function(ContainerConfigurator $containerConfigurator, ContainerBuilder $container)
Definition: Services.php:11
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getProperty
‪static mixed getProperty($subject, string $propertyName)
Definition: ObjectAccess.php:61
‪TYPO3\CMS\Extbase\Reflection\Exception\PropertyNotAccessibleException
Definition: PropertyNotAccessibleException.php:25
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\createAccessor
‪static PropertyAccessor createAccessor()
Definition: ObjectAccess.php:383
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\wrap
‪static string wrap(string $segment)
Definition: ObjectAccess.php:438
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getGettablePropertyNames
‪static array getGettablePropertyNames(object $object)
Definition: ObjectAccess.php:200
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess
Definition: ObjectAccess.php:39
‪TYPO3\CMS\Extbase\Persistence\ObjectStorage
Definition: ObjectStorage.php:32
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getObjectPropertyValue
‪static mixed getObjectPropertyValue(object $subject, PropertyPath $propertyPath)
Definition: ObjectAccess.php:400
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\isPropertyGettable
‪static bool isPropertyGettable($object, $propertyName)
Definition: ObjectAccess.php:352
‪TYPO3\CMS\Extbase\Reflection
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getPropertyInternal
‪static mixed getPropertyInternal($subject, string $propertyName)
Definition: ObjectAccess.php:90
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getGettableProperties
‪static array getGettableProperties(object $object)
Definition: ObjectAccess.php:371
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getSettablePropertyNames
‪static array getSettablePropertyNames(object $object)
Definition: ObjectAccess.php:296
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getArrayIndexValue
‪static mixed getArrayIndexValue(array $subject, PropertyPath $propertyPath)
Definition: ObjectAccess.php:416
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\getPropertyPath
‪static mixed getPropertyPath($subject, string $propertyPath)
Definition: ObjectAccess.php:140
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\setProperty
‪static bool setProperty(&$subject, string $propertyName, $propertyValue)
Definition: ObjectAccess.php:170
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\isPropertySettable
‪static bool isPropertySettable(object $object, $propertyName)
Definition: ObjectAccess.php:332
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50