[BUGFIX] Fix several typos in php comments
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Reflection / ObjectAccess.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Extbase\Reflection;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use Symfony\Component\PropertyAccess\PropertyAccess;
20 use Symfony\Component\PropertyAccess\PropertyAccessor;
21 use Symfony\Component\PropertyAccess\PropertyPath;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Utility\StringUtility;
24 use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
25
26 /**
27 * Provides methods to call appropriate getter/setter on an object given the
28 * property name. It does this following these rules:
29 * - if the target object is an instance of ArrayAccess, it gets/sets the property
30 * - if public getter/setter method exists, call it.
31 * - if public property exists, return/set the value of it.
32 * - else, throw exception
33 * @internal only to be used within Extbase, not part of TYPO3 Core API.
34 */
35 class ObjectAccess
36 {
37 /**
38 * @var PropertyAccessor
39 */
40 private static $propertyAccessor;
41
42 /**
43 * Get a property of a given object.
44 * Tries to get the property the following ways:
45 * - if the target is an array, and has this property, we call it.
46 * - if super cow powers should be used, fetch value through reflection
47 * - if public getter method exists, call it.
48 * - if the target object is an instance of ArrayAccess, it gets the property
49 * on it if it exists.
50 * - if public property exists, return the value of it.
51 * - else, throw exception
52 *
53 * @param mixed $subject Object or array to get the property from
54 * @param string $propertyName name of the property to retrieve
55 * @param bool $forceDirectAccess directly access property using reflection(!)
56 *
57 * @throws \InvalidArgumentException in case $subject was not an object or $propertyName was not a string
58 * @throws Exception\PropertyNotAccessibleException
59 * @return mixed Value of the property
60 */
61 public static function getProperty($subject, string $propertyName, bool $forceDirectAccess = false)
62 {
63 if (!is_object($subject) && !is_array($subject)) {
64 throw new \InvalidArgumentException(
65 '$subject must be an object or array, ' . gettype($subject) . ' given.',
66 1237301367
67 );
68 }
69
70 return self::getPropertyInternal($subject, $propertyName, $forceDirectAccess);
71 }
72
73 /**
74 * Gets a property of a given object or array.
75 * This is an internal method that does only limited type checking for performance reasons.
76 * If you can't make sure that $subject is either of type array or object and $propertyName of type string you should use getProperty() instead.
77 *
78 * @see getProperty()
79 *
80 * @param mixed $subject Object or array to get the property from
81 * @param string $propertyName name of the property to retrieve
82 * @param bool $forceDirectAccess directly access property using reflection(!)
83 *
84 * @throws Exception\PropertyNotAccessibleException
85 * @return mixed Value of the property
86 * @internal
87 */
88 public static function getPropertyInternal($subject, string $propertyName, bool $forceDirectAccess = false)
89 {
90 if ($forceDirectAccess === true) {
91 trigger_error('Argument $forceDirectAccess will be removed in TYPO3 11.0', E_USER_DEPRECATED);
92 }
93
94 if (!$forceDirectAccess && ($subject instanceof \SplObjectStorage || $subject instanceof ObjectStorage)) {
95 $subject = iterator_to_array(clone $subject, false);
96 }
97
98 $propertyPath = new PropertyPath($propertyName);
99
100 if ($subject instanceof \ArrayAccess) {
101 $accessor = self::createAccessor();
102
103 // Check if $subject is an instance of \ArrayAccess and therefore maybe has actual accessible properties.
104 if ($accessor->isReadable($subject, $propertyPath)) {
105 return $accessor->getValue($subject, $propertyPath);
106 }
107
108 // Use array style property path for instances of \ArrayAccess
109 // https://symfony.com/doc/current/components/property_access.html#reading-from-arrays
110
111 $propertyPath = self::convertToArrayPropertyPath($propertyPath);
112 }
113
114 if (is_object($subject)) {
115 return self::getObjectPropertyValue($subject, $propertyPath, $forceDirectAccess);
116 }
117
118 if (is_array($subject)) {
119 return self::getArrayIndexValue($subject, self::convertToArrayPropertyPath($propertyPath));
120 }
121
122 return null;
123 }
124
125 /**
126 * Gets a property path from a given object or array.
127 *
128 * If propertyPath is "bla.blubb", then we first call getProperty($object, 'bla'),
129 * and on the resulting object we call getProperty(..., 'blubb')
130 *
131 * For arrays the keys are checked likewise.
132 *
133 * @param mixed $subject Object or array to get the property path from
134 * @param string $propertyPath
135 *
136 * @return mixed Value of the property
137 */
138 public static function getPropertyPath($subject, string $propertyPath)
139 {
140 try {
141 foreach (new PropertyPath($propertyPath) as $pathSegment) {
142 $subject = self::getPropertyInternal($subject, $pathSegment);
143 }
144 } catch (Exception\PropertyNotAccessibleException $error) {
145 return null;
146 }
147 return $subject;
148 }
149
150 /**
151 * Set a property for a given object.
152 * Tries to set the property the following ways:
153 * - if target is an array, set value
154 * - if super cow powers should be used, set value through reflection
155 * - if public setter method exists, call it.
156 * - if public property exists, set it directly.
157 * - if the target object is an instance of ArrayAccess, it sets the property
158 * on it without checking if it existed.
159 * - else, return FALSE
160 *
161 * @param mixed &$subject The target object or array
162 * @param string $propertyName Name of the property to set
163 * @param mixed $propertyValue Value of the property
164 * @param bool $forceDirectAccess directly access property using reflection(!)
165 *
166 * @throws \InvalidArgumentException in case $object was not an object or $propertyName was not a string
167 * @return bool TRUE if the property could be set, FALSE otherwise
168 */
169 public static function setProperty(&$subject, string $propertyName, $propertyValue, bool $forceDirectAccess = false): bool
170 {
171 if ($forceDirectAccess === true) {
172 trigger_error('Argument $forceDirectAccess will be removed in TYPO3 11.0', E_USER_DEPRECATED);
173 }
174
175 if (is_array($subject) || ($subject instanceof \ArrayAccess && !$forceDirectAccess)) {
176 $subject[$propertyName] = $propertyValue;
177 return true;
178 }
179 if (!is_object($subject)) {
180 throw new \InvalidArgumentException('subject must be an object or array, ' . gettype($subject) . ' given.', 1237301368);
181 }
182
183 $accessor = self::createAccessor();
184 if ($accessor->isWritable($subject, $propertyName)) {
185 $accessor->setValue($subject, $propertyName, $propertyValue);
186 return true;
187 }
188
189 if ($forceDirectAccess) {
190 if (property_exists($subject, $propertyName)) {
191 $propertyReflection = new \ReflectionProperty($subject, $propertyName);
192 $propertyReflection->setAccessible(true);
193 $propertyReflection->setValue($subject, $propertyValue);
194 } else {
195 $subject->{$propertyName} = $propertyValue;
196 }
197
198 return true;
199 }
200
201 return false;
202 }
203
204 /**
205 * Returns an array of properties which can be get with the getProperty()
206 * method.
207 * Includes the following properties:
208 * - which can be get through a public getter method.
209 * - public properties which can be directly get.
210 *
211 * @param object $object Object to receive property names for
212 *
213 * @return array Array of all gettable property names
214 * @throws Exception\UnknownClassException
215 */
216 public static function getGettablePropertyNames(object $object): array
217 {
218 if ($object instanceof \stdClass) {
219 $properties = array_keys((array)$object);
220 sort($properties);
221 return $properties;
222 }
223
224 $classSchema = GeneralUtility::makeInstance(ReflectionService::class)
225 ->getClassSchema($object);
226
227 $accessor = self::createAccessor();
228 $propertyNames = array_keys($classSchema->getProperties());
229 $accessiblePropertyNames = array_filter($propertyNames, function ($propertyName) use ($accessor, $object) {
230 return $accessor->isReadable($object, $propertyName);
231 });
232
233 foreach ($classSchema->getMethods() as $methodName => $methodDefinition) {
234 if (!$methodDefinition->isPublic()) {
235 continue;
236 }
237
238 foreach ($methodDefinition->getParameters() as $methodParam) {
239 if (!$methodParam->isOptional()) {
240 continue 2;
241 }
242 }
243
244 if (StringUtility::beginsWith($methodName, 'get')) {
245 $accessiblePropertyNames[] = lcfirst(substr($methodName, 3));
246 continue;
247 }
248
249 if (StringUtility::beginsWith($methodName, 'has')) {
250 $accessiblePropertyNames[] = lcfirst(substr($methodName, 3));
251 continue;
252 }
253
254 if (StringUtility::beginsWith($methodName, 'is')) {
255 $accessiblePropertyNames[] = lcfirst(substr($methodName, 2));
256 }
257 }
258
259 $accessiblePropertyNames = array_unique($accessiblePropertyNames);
260 sort($accessiblePropertyNames);
261 return $accessiblePropertyNames;
262 }
263
264 /**
265 * Returns an array of properties which can be set with the setProperty()
266 * method.
267 * Includes the following properties:
268 * - which can be set through a public setter method.
269 * - public properties which can be directly set.
270 *
271 * @param object $object Object to receive property names for
272 *
273 * @throws \InvalidArgumentException
274 * @return array Array of all settable property names
275 */
276 public static function getSettablePropertyNames(object $object): array
277 {
278 $accessor = self::createAccessor();
279
280 if ($object instanceof \stdClass || $object instanceof \ArrayAccess) {
281 $propertyNames = array_keys((array)$object);
282 } else {
283 $classSchema = GeneralUtility::makeInstance(ReflectionService::class)->getClassSchema($object);
284
285 $propertyNames = array_filter(array_keys($classSchema->getProperties()), function ($methodName) use ($accessor, $object) {
286 return $accessor->isWritable($object, $methodName);
287 });
288
289 $setters = array_filter(array_keys($classSchema->getMethods()), function ($methodName) use ($object) {
290 return StringUtility::beginsWith($methodName, 'set') && is_callable([$object, $methodName]);
291 });
292
293 foreach ($setters as $setter) {
294 $propertyNames[] = lcfirst(substr($setter, 3));
295 }
296 }
297
298 $propertyNames = array_unique($propertyNames);
299 sort($propertyNames);
300 return $propertyNames;
301 }
302
303 /**
304 * Tells if the value of the specified property can be set by this Object Accessor.
305 *
306 * @param object $object Object containing the property
307 * @param string $propertyName Name of the property to check
308 *
309 * @throws \InvalidArgumentException
310 * @return bool
311 */
312 public static function isPropertySettable(object $object, $propertyName): bool
313 {
314 if ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) {
315 return true;
316 }
317 if (array_key_exists($propertyName, get_class_vars(get_class($object)))) {
318 return true;
319 }
320 return is_callable([$object, 'set' . ucfirst($propertyName)]);
321 }
322
323 /**
324 * Tells if the value of the specified property can be retrieved by this Object Accessor.
325 *
326 * @param object $object Object containing the property
327 * @param string $propertyName Name of the property to check
328 *
329 * @throws \InvalidArgumentException
330 * @return bool
331 */
332 public static function isPropertyGettable($object, $propertyName): bool
333 {
334 if (($object instanceof \ArrayAccess) && !$object->offsetExists($propertyName)) {
335 return false;
336 }
337
338 if (is_array($object) || $object instanceof \ArrayAccess) {
339 $propertyName = self::wrap($propertyName);
340 }
341
342 return self::createAccessor()->isReadable($object, $propertyName);
343 }
344
345 /**
346 * Get all properties (names and their current values) of the current
347 * $object that are accessible through this class.
348 *
349 * @param object $object Object to get all properties from.
350 *
351 * @throws \InvalidArgumentException
352 * @return array Associative array of all properties.
353 * @todo What to do with ArrayAccess
354 */
355 public static function getGettableProperties(object $object): array
356 {
357 $properties = [];
358 foreach (self::getGettablePropertyNames($object) as $propertyName) {
359 $properties[$propertyName] = self::getPropertyInternal($object, $propertyName);
360 }
361 return $properties;
362 }
363
364 /**
365 * Build the setter method name for a given property by capitalizing the
366 * first letter of the property, and prepending it with "set".
367 *
368 * @param string $propertyName Name of the property
369 *
370 * @return string Name of the setter method name
371 * @deprecated
372 */
373 public static function buildSetterMethodName($propertyName): string
374 {
375 trigger_error(__METHOD__ . ' will be removed in TYPO3 11.0', E_USER_DEPRECATED);
376
377 return 'set' . ucfirst($propertyName);
378 }
379
380 /**
381 * @return PropertyAccessor
382 */
383 private static function createAccessor(): PropertyAccessor
384 {
385 if (static::$propertyAccessor === null) {
386 static::$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
387 ->getPropertyAccessor();
388 }
389
390 return static::$propertyAccessor;
391 }
392
393 /**
394 * @param object $subject
395 * @param PropertyPath $propertyPath
396 * @param bool $forceDirectAccess
397 * @return mixed
398 * @throws Exception\PropertyNotAccessibleException
399 * @throws \ReflectionException
400 */
401 private static function getObjectPropertyValue(object $subject, PropertyPath $propertyPath, bool $forceDirectAccess)
402 {
403 $accessor = self::createAccessor();
404
405 if ($accessor->isReadable($subject, $propertyPath)) {
406 return $accessor->getValue($subject, $propertyPath);
407 }
408
409 $propertyName = (string)$propertyPath;
410
411 if (!$forceDirectAccess) {
412 throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1476109666);
413 }
414
415 if (!property_exists($subject, $propertyName)) {
416 throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1302855001);
417 }
418
419 $propertyReflection = new \ReflectionProperty($subject, $propertyName);
420 $propertyReflection->setAccessible(true);
421 return $propertyReflection->getValue($subject);
422 }
423
424 /**
425 * @param array $subject
426 * @param PropertyPath $propertyPath
427 * @return mixed
428 */
429 private static function getArrayIndexValue(array $subject, PropertyPath $propertyPath)
430 {
431 return self::createAccessor()->getValue($subject, $propertyPath);
432 }
433
434 /**
435 * @param PropertyPath $propertyPath
436 * @return PropertyPath
437 */
438 private static function convertToArrayPropertyPath(PropertyPath $propertyPath): PropertyPath
439 {
440 $segments = array_map(function ($segment) {
441 return static::wrap($segment);
442 }, $propertyPath->getElements());
443
444 return new PropertyPath(implode('.', $segments));
445 }
446
447 /**
448 * @param string $segment
449 * @return string
450 */
451 private static function wrap(string $segment): string
452 {
453 return '[' . $segment . ']';
454 }
455 }