[TASK] Streamline phpdoc annotations in EXT:extbase
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Reflection / ObjectAccess.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Reflection;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
18
19 /**
20 * Provides methods to call appropriate getter/setter on an object given the
21 * property name. It does this following these rules:
22 * - if the target object is an instance of ArrayAccess, it gets/sets the property
23 * - if public getter/setter method exists, call it.
24 * - if public property exists, return/set the value of it.
25 * - else, throw exception
26 * @internal only to be used within Extbase, not part of TYPO3 Core API.
27 */
28 class ObjectAccess
29 {
30 const ACCESS_GET = 0;
31
32 const ACCESS_SET = 1;
33
34 const ACCESS_PUBLIC = 2;
35
36 /**
37 * Get a property of a given object.
38 * Tries to get the property the following ways:
39 * - if the target is an array, and has this property, we call it.
40 * - if super cow powers should be used, fetch value through reflection
41 * - if public getter method exists, call it.
42 * - if the target object is an instance of ArrayAccess, it gets the property
43 * on it if it exists.
44 * - if public property exists, return the value of it.
45 * - else, throw exception
46 *
47 * @param mixed $subject Object or array to get the property from
48 * @param string $propertyName name of the property to retrieve
49 * @param bool $forceDirectAccess directly access property using reflection(!)
50 *
51 * @throws \InvalidArgumentException in case $subject was not an object or $propertyName was not a string
52 * @return mixed Value of the property
53 */
54 public static function getProperty($subject, $propertyName, $forceDirectAccess = false)
55 {
56 if (!is_object($subject) && !is_array($subject)) {
57 throw new \InvalidArgumentException('$subject must be an object or array, ' . gettype($subject) . ' given.', 1237301367);
58 }
59 if (!is_string($propertyName) && (!is_array($subject) && !$subject instanceof \ArrayAccess)) {
60 throw new \InvalidArgumentException('Given property name is not of type string.', 1231178303);
61 }
62 return self::getPropertyInternal($subject, $propertyName, $forceDirectAccess);
63 }
64
65 /**
66 * Gets a property of a given object or array.
67 * This is an internal method that does only limited type checking for performance reasons.
68 * 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.
69 *
70 * @see getProperty()
71 *
72 * @param mixed $subject Object or array to get the property from
73 * @param string $propertyName name of the property to retrieve
74 * @param bool $forceDirectAccess directly access property using reflection(!)
75 *
76 * @throws Exception\PropertyNotAccessibleException
77 * @return mixed Value of the property
78 * @internal
79 */
80 public static function getPropertyInternal($subject, $propertyName, $forceDirectAccess = false)
81 {
82 // type check and conversion of iterator to numerically indexed array
83 if ($subject === null || is_scalar($subject)) {
84 return null;
85 }
86 if (!$forceDirectAccess && ($subject instanceof \SplObjectStorage || $subject instanceof ObjectStorage)) {
87 $subject = iterator_to_array(clone $subject, false);
88 }
89
90 // value get based on data type of $subject (possibly converted above)
91 if (($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)) || is_array($subject)) {
92 // isset() is safe; array_key_exists would only be needed to determine
93 // if the value is NULL - and that's already what we return as fallback.
94 if (isset($subject[$propertyName])) {
95 return $subject[$propertyName];
96 }
97 } elseif (is_object($subject)) {
98 if ($forceDirectAccess) {
99 if (property_exists($subject, $propertyName)) {
100 $propertyReflection = new \ReflectionProperty($subject, $propertyName);
101 if ($propertyReflection->isPublic()) {
102 return $propertyReflection->getValue($subject);
103 }
104 $propertyReflection->setAccessible(true);
105 return $propertyReflection->getValue($subject);
106 }
107 throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1302855001);
108 }
109 $upperCasePropertyName = ucfirst($propertyName);
110 $getterMethodName = 'get' . $upperCasePropertyName;
111 if (is_callable([$subject, $getterMethodName])) {
112 return $subject->{$getterMethodName}();
113 }
114 $getterMethodName = 'is' . $upperCasePropertyName;
115 if (is_callable([$subject, $getterMethodName])) {
116 return $subject->{$getterMethodName}();
117 }
118 $getterMethodName = 'has' . $upperCasePropertyName;
119 if (is_callable([$subject, $getterMethodName])) {
120 return $subject->{$getterMethodName}();
121 }
122 if (property_exists($subject, $propertyName)) {
123 return $subject->{$propertyName};
124 }
125 throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1476109666);
126 }
127
128 return null;
129 }
130
131 /**
132 * Gets a property path from a given object or array.
133 *
134 * If propertyPath is "bla.blubb", then we first call getProperty($object, 'bla'),
135 * and on the resulting object we call getProperty(..., 'blubb')
136 *
137 * For arrays the keys are checked likewise.
138 *
139 * @param mixed $subject Object or array to get the property path from
140 * @param string $propertyPath
141 *
142 * @return mixed Value of the property
143 */
144 public static function getPropertyPath($subject, $propertyPath)
145 {
146 $propertyPathSegments = explode('.', $propertyPath);
147 try {
148 foreach ($propertyPathSegments as $pathSegment) {
149 $subject = self::getPropertyInternal($subject, $pathSegment);
150 }
151 } catch (Exception\PropertyNotAccessibleException $error) {
152 return null;
153 }
154 return $subject;
155 }
156
157 /**
158 * Set a property for a given object.
159 * Tries to set the property the following ways:
160 * - if target is an array, set value
161 * - if super cow powers should be used, set value through reflection
162 * - if public setter method exists, call it.
163 * - if public property exists, set it directly.
164 * - if the target object is an instance of ArrayAccess, it sets the property
165 * on it without checking if it existed.
166 * - else, return FALSE
167 *
168 * @param mixed &$subject The target object or array
169 * @param string $propertyName Name of the property to set
170 * @param mixed $propertyValue Value of the property
171 * @param bool $forceDirectAccess directly access property using reflection(!)
172 *
173 * @throws \InvalidArgumentException in case $object was not an object or $propertyName was not a string
174 * @return bool TRUE if the property could be set, FALSE otherwise
175 */
176 public static function setProperty(&$subject, $propertyName, $propertyValue, $forceDirectAccess = false)
177 {
178 if (is_array($subject) || ($subject instanceof \ArrayAccess && !$forceDirectAccess)) {
179 $subject[$propertyName] = $propertyValue;
180 return true;
181 }
182 if (!is_object($subject)) {
183 throw new \InvalidArgumentException('subject must be an object or array, ' . gettype($subject) . ' given.', 1237301368);
184 }
185 if (!is_string($propertyName)) {
186 throw new \InvalidArgumentException('Given property name is not of type string.', 1231178878);
187 }
188 $result = true;
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 return $result;
198 }
199 $setterMethodName = self::buildSetterMethodName($propertyName);
200 if (is_callable([$subject, $setterMethodName])) {
201 $subject->{$setterMethodName}($propertyValue);
202 } elseif (property_exists($subject, $propertyName)) {
203 $reflection = new \ReflectionProperty($subject, $propertyName);
204 if ($reflection->isPublic()) {
205 $subject->{$propertyName} = $propertyValue;
206 } else {
207 $result = false;
208 }
209 } else {
210 $result = false;
211 }
212 return $result;
213 }
214
215 /**
216 * Returns an array of properties which can be get with the getProperty()
217 * method.
218 * Includes the following properties:
219 * - which can be get through a public getter method.
220 * - public properties which can be directly get.
221 *
222 * @param object $object Object to receive property names for
223 *
224 * @throws \InvalidArgumentException
225 * @return array Array of all gettable property names
226 */
227 public static function getGettablePropertyNames($object)
228 {
229 if (!is_object($object)) {
230 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1237301369);
231 }
232 if ($object instanceof \stdClass) {
233 $properties = array_keys((array)$object);
234 sort($properties);
235 return $properties;
236 }
237
238 $reflection = new \ReflectionClass($object);
239 $declaredPropertyNames = array_map(
240 function (\ReflectionProperty $property) {
241 return $property->getName();
242 },
243 $reflection->getProperties(\ReflectionProperty::IS_PUBLIC)
244 );
245 foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
246 $methodParameters = $method->getParameters();
247 if (!empty($methodParameters)) {
248 foreach ($methodParameters as $parameter) {
249 if (!$parameter->isOptional()) {
250 continue 2;
251 }
252 }
253 }
254 $methodName = $method->getName();
255 if (strpos($methodName, 'is') === 0) {
256 $declaredPropertyNames[] = lcfirst(substr($methodName, 2));
257 }
258 if (strpos($methodName, 'get') === 0) {
259 $declaredPropertyNames[] = lcfirst(substr($methodName, 3));
260 }
261 if (strpos($methodName, 'has') === 0) {
262 $declaredPropertyNames[] = lcfirst(substr($methodName, 3));
263 }
264 }
265 $propertyNames = array_unique($declaredPropertyNames);
266 sort($propertyNames);
267
268 return $propertyNames;
269 }
270
271 /**
272 * Returns an array of properties which can be set with the setProperty()
273 * method.
274 * Includes the following properties:
275 * - which can be set through a public setter method.
276 * - public properties which can be directly set.
277 *
278 * @param object $object Object to receive property names for
279 *
280 * @throws \InvalidArgumentException
281 * @return array Array of all settable property names
282 */
283 public static function getSettablePropertyNames($object)
284 {
285 if (!is_object($object)) {
286 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1264022994);
287 }
288 if ($object instanceof \stdClass) {
289 $declaredPropertyNames = array_keys((array)$object);
290 } else {
291 $declaredPropertyNames = array_keys(get_class_vars(get_class($object)));
292 }
293 foreach (get_class_methods($object) as $methodName) {
294 if (strpos($methodName, 'set') === 0 && is_callable([$object, $methodName])) {
295 $declaredPropertyNames[] = lcfirst(substr($methodName, 3));
296 }
297 }
298 $propertyNames = array_unique($declaredPropertyNames);
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 containting 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, $propertyName)
313 {
314 if (!is_object($object)) {
315 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1259828920);
316 }
317 if ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) {
318 return true;
319 }
320 if (array_key_exists($propertyName, get_class_vars(get_class($object)))) {
321 return true;
322 }
323 return is_callable([$object, self::buildSetterMethodName($propertyName)]);
324 }
325
326 /**
327 * Tells if the value of the specified property can be retrieved by this Object Accessor.
328 *
329 * @param object $object Object containting the property
330 * @param string $propertyName Name of the property to check
331 *
332 * @throws \InvalidArgumentException
333 * @return bool
334 */
335 public static function isPropertyGettable($object, $propertyName)
336 {
337 if (!is_object($object)) {
338 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1259828921);
339 }
340 if ($object instanceof \ArrayAccess && isset($object[$propertyName])) {
341 return true;
342 }
343 if ($object instanceof \stdClass && isset($object->$propertyName)) {
344 return true;
345 }
346 if (is_callable([$object, 'get' . ucfirst($propertyName)])) {
347 return true;
348 }
349 if (is_callable([$object, 'has' . ucfirst($propertyName)])) {
350 return true;
351 }
352 if (is_callable([$object, 'is' . ucfirst($propertyName)])) {
353 return true;
354 }
355 if (property_exists($object, $propertyName)) {
356 $propertyReflection = new \ReflectionProperty($object, $propertyName);
357 return $propertyReflection->isPublic();
358 }
359 return false;
360 }
361
362 /**
363 * Get all properties (names and their current values) of the current
364 * $object that are accessible through this class.
365 *
366 * @param object $object Object to get all properties from.
367 *
368 * @throws \InvalidArgumentException
369 * @return array Associative array of all properties.
370 * @todo What to do with ArrayAccess
371 */
372 public static function getGettableProperties($object)
373 {
374 if (!is_object($object)) {
375 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1237301370);
376 }
377 $properties = [];
378 foreach (self::getGettablePropertyNames($object) as $propertyName) {
379 $properties[$propertyName] = self::getPropertyInternal($object, $propertyName);
380 }
381 return $properties;
382 }
383
384 /**
385 * Build the setter method name for a given property by capitalizing the
386 * first letter of the property, and prepending it with "set".
387 *
388 * @param string $propertyName Name of the property
389 *
390 * @return string Name of the setter method name
391 */
392 public static function buildSetterMethodName($propertyName)
393 {
394 return 'set' . ucfirst($propertyName);
395 }
396 }