[FEATURE] Support 'has*' for properties in ObjectAccess
[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\Core\Utility\MathUtility;
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 */
27 class ObjectAccess {
28
29 const ACCESS_GET = 0;
30
31 const ACCESS_SET = 1;
32
33 const ACCESS_PUBLIC = 2;
34
35 /**
36 * Get a property of a given object.
37 * Tries to get the property the following ways:
38 * - if the target is an array, and has this property, we call it.
39 * - if super cow powers should be used, fetch value through reflection
40 * - if public getter method exists, call it.
41 * - if the target object is an instance of ArrayAccess, it gets the property
42 * on it if it exists.
43 * - if public property exists, return the value of it.
44 * - else, throw exception
45 *
46 * @param mixed $subject Object or array to get the property from
47 * @param string $propertyName name of the property to retrieve
48 * @param bool $forceDirectAccess directly access property using reflection(!)
49 *
50 * @throws Exception\PropertyNotAccessibleException
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 static public function getProperty($subject, $propertyName, $forceDirectAccess = FALSE) {
55 if (!is_object($subject) && !is_array($subject)) {
56 throw new \InvalidArgumentException('$subject must be an object or array, ' . gettype($subject) . ' given.', 1237301367);
57 }
58 if (!is_string($propertyName) && (!is_array($subject) && !$subject instanceof \ArrayAccess)) {
59 throw new \InvalidArgumentException('Given property name is not of type string.', 1231178303);
60 }
61 $propertyExists = FALSE;
62 $propertyValue = self::getPropertyInternal($subject, $propertyName, $forceDirectAccess, $propertyExists);
63 if ($propertyExists === TRUE) {
64 return $propertyValue;
65 }
66 throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject was not accessible.', 1263391473);
67 }
68
69 /**
70 * Gets a property of a given object or array.
71 * This is an internal method that does only limited type checking for performance reasons.
72 * 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.
73 *
74 * @see getProperty()
75 *
76 * @param mixed $subject Object or array to get the property from
77 * @param string $propertyName name of the property to retrieve
78 * @param bool $forceDirectAccess directly access property using reflection(!)
79 * @param bool &$propertyExists (by reference) will be set to TRUE if the specified property exists and is gettable
80 *
81 * @throws Exception\PropertyNotAccessibleException
82 * @return mixed Value of the property
83 * @internal
84 */
85 static public function getPropertyInternal($subject, $propertyName, $forceDirectAccess, &$propertyExists) {
86 if ($subject === NULL || is_scalar($subject)) {
87 return NULL;
88 }
89 $propertyExists = TRUE;
90 if (is_array($subject)) {
91 if (array_key_exists($propertyName, $subject)) {
92 return $subject[$propertyName];
93 }
94 $propertyExists = FALSE;
95 return NULL;
96 }
97 if ($forceDirectAccess === TRUE) {
98 if (property_exists(get_class($subject), $propertyName)) {
99 $propertyReflection = new PropertyReflection(get_class($subject), $propertyName);
100 return $propertyReflection->getValue($subject);
101 } elseif (property_exists($subject, $propertyName)) {
102 return $subject->{$propertyName};
103 } else {
104 throw new Exception\PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1302855001);
105 }
106 }
107 if ($subject instanceof \SplObjectStorage || $subject instanceof \TYPO3\CMS\Extbase\Persistence\ObjectStorage) {
108 if (MathUtility::canBeInterpretedAsInteger($propertyName)) {
109 $index = 0;
110 foreach ($subject as $value) {
111 if ($index === (int)$propertyName) {
112 return $value;
113 }
114 $index++;
115 }
116 $propertyExists = FALSE;
117 return NULL;
118 }
119 } elseif ($subject instanceof \ArrayAccess && isset($subject[$propertyName])) {
120 return $subject[$propertyName];
121 }
122 $getterMethodName = 'get' . ucfirst($propertyName);
123 if (is_callable(array($subject, $getterMethodName))) {
124 return $subject->{$getterMethodName}();
125 }
126 $getterMethodName = 'is' . ucfirst($propertyName);
127 if (is_callable(array($subject, $getterMethodName))) {
128 return $subject->{$getterMethodName}();
129 }
130 $getterMethodName = 'has' . ucfirst($propertyName);
131 if (is_callable(array($subject, $getterMethodName))) {
132 return $subject->{$getterMethodName}();
133 }
134 if (is_object($subject) && array_key_exists($propertyName, get_object_vars($subject))) {
135 return $subject->{$propertyName};
136 }
137 $propertyExists = FALSE;
138 return NULL;
139 }
140
141 /**
142 * Gets a property path from a given object or array.
143 *
144 * If propertyPath is "bla.blubb", then we first call getProperty($object, 'bla'),
145 * and on the resulting object we call getProperty(..., 'blubb')
146 *
147 * For arrays the keys are checked likewise.
148 *
149 * @param mixed $subject Object or array to get the property path from
150 * @param string $propertyPath
151 *
152 * @return mixed Value of the property
153 */
154 static public function getPropertyPath($subject, $propertyPath) {
155 $propertyPathSegments = explode('.', $propertyPath);
156 foreach ($propertyPathSegments as $pathSegment) {
157 $propertyExists = FALSE;
158 $subject = self::getPropertyInternal($subject, $pathSegment, FALSE, $propertyExists);
159 if (!$propertyExists || $subject === NULL) {
160 return $subject;
161 }
162 }
163 return $subject;
164 }
165
166 /**
167 * Set a property for a given object.
168 * Tries to set the property the following ways:
169 * - if target is an array, set value
170 * - if super cow powers should be used, set value through reflection
171 * - if public setter method exists, call it.
172 * - if public property exists, set it directly.
173 * - if the target object is an instance of ArrayAccess, it sets the property
174 * on it without checking if it existed.
175 * - else, return FALSE
176 *
177 * @param mixed &$subject The target object or array
178 * @param string $propertyName Name of the property to set
179 * @param mixed $propertyValue Value of the property
180 * @param bool $forceDirectAccess directly access property using reflection(!)
181 *
182 * @throws \InvalidArgumentException in case $object was not an object or $propertyName was not a string
183 * @return bool TRUE if the property could be set, FALSE otherwise
184 */
185 static public function setProperty(&$subject, $propertyName, $propertyValue, $forceDirectAccess = FALSE) {
186 if (is_array($subject)) {
187 $subject[$propertyName] = $propertyValue;
188 return TRUE;
189 }
190 if (!is_object($subject)) {
191 throw new \InvalidArgumentException('subject must be an object or array, ' . gettype($subject) . ' given.', 1237301368);
192 }
193 if (!is_string($propertyName)) {
194 throw new \InvalidArgumentException('Given property name is not of type string.', 1231178878);
195 }
196 if ($forceDirectAccess === TRUE) {
197 if (property_exists(get_class($subject), $propertyName)) {
198 $propertyReflection = new PropertyReflection(get_class($subject), $propertyName);
199 $propertyReflection->setAccessible(TRUE);
200 $propertyReflection->setValue($subject, $propertyValue);
201 } else {
202 $subject->{$propertyName} = $propertyValue;
203 }
204 } elseif (is_callable(array($subject, $setterMethodName = self::buildSetterMethodName($propertyName)))) {
205 $subject->{$setterMethodName}($propertyValue);
206 } elseif ($subject instanceof \ArrayAccess) {
207 $subject[$propertyName] = $propertyValue;
208 } elseif (array_key_exists($propertyName, get_object_vars($subject))) {
209 $subject->{$propertyName} = $propertyValue;
210 } else {
211 return FALSE;
212 }
213 return TRUE;
214 }
215
216 /**
217 * Returns an array of properties which can be get with the getProperty()
218 * method.
219 * Includes the following properties:
220 * - which can be get through a public getter method.
221 * - public properties which can be directly get.
222 *
223 * @param object $object Object to receive property names for
224 *
225 * @throws \InvalidArgumentException
226 * @return array Array of all gettable property names
227 */
228 static public function getGettablePropertyNames($object) {
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 $declaredPropertyNames = array_keys(get_object_vars($object));
234 } else {
235 $declaredPropertyNames = array_keys(get_class_vars(get_class($object)));
236 }
237 foreach (get_class_methods($object) as $methodName) {
238 if (is_callable(array($object, $methodName))) {
239 if (substr($methodName, 0, 2) === 'is') {
240 $declaredPropertyNames[] = lcfirst(substr($methodName, 2));
241 }
242 if (substr($methodName, 0, 3) === 'get') {
243 $declaredPropertyNames[] = lcfirst(substr($methodName, 3));
244 }
245 if (substr($methodName, 0, 3) === 'has') {
246 $declaredPropertyNames[] = lcfirst(substr($methodName, 3));
247 }
248 }
249 }
250 $propertyNames = array_unique($declaredPropertyNames);
251 sort($propertyNames);
252 return $propertyNames;
253 }
254
255 /**
256 * Returns an array of properties which can be set with the setProperty()
257 * method.
258 * Includes the following properties:
259 * - which can be set through a public setter method.
260 * - public properties which can be directly set.
261 *
262 * @param object $object Object to receive property names for
263 *
264 * @throws \InvalidArgumentException
265 * @return array Array of all settable property names
266 */
267 static public function getSettablePropertyNames($object) {
268 if (!is_object($object)) {
269 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1264022994);
270 }
271 if ($object instanceof \stdClass) {
272 $declaredPropertyNames = array_keys(get_object_vars($object));
273 } else {
274 $declaredPropertyNames = array_keys(get_class_vars(get_class($object)));
275 }
276 foreach (get_class_methods($object) as $methodName) {
277 if (substr($methodName, 0, 3) === 'set' && is_callable(array($object, $methodName))) {
278 $declaredPropertyNames[] = lcfirst(substr($methodName, 3));
279 }
280 }
281 $propertyNames = array_unique($declaredPropertyNames);
282 sort($propertyNames);
283 return $propertyNames;
284 }
285
286 /**
287 * Tells if the value of the specified property can be set by this Object Accessor.
288 *
289 * @param object $object Object containting the property
290 * @param string $propertyName Name of the property to check
291 *
292 * @throws \InvalidArgumentException
293 * @return bool
294 */
295 static public function isPropertySettable($object, $propertyName) {
296 if (!is_object($object)) {
297 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1259828920);
298 }
299 if ($object instanceof \stdClass && array_search($propertyName, array_keys(get_object_vars($object))) !== FALSE) {
300 return TRUE;
301 } elseif (array_search($propertyName, array_keys(get_class_vars(get_class($object)))) !== FALSE) {
302 return TRUE;
303 }
304 return is_callable(array($object, self::buildSetterMethodName($propertyName)));
305 }
306
307 /**
308 * Tells if the value of the specified property can be retrieved by this Object Accessor.
309 *
310 * @param object $object Object containting the property
311 * @param string $propertyName Name of the property to check
312 *
313 * @throws \InvalidArgumentException
314 * @return bool
315 */
316 static public function isPropertyGettable($object, $propertyName) {
317 if (!is_object($object)) {
318 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1259828921);
319 }
320 if ($object instanceof \ArrayAccess && isset($object[$propertyName]) === TRUE) {
321 return TRUE;
322 } elseif ($object instanceof \stdClass && array_search($propertyName, array_keys(get_object_vars($object))) !== FALSE) {
323 return TRUE;
324 } elseif ($object instanceof \ArrayAccess && isset($object[$propertyName]) === TRUE) {
325 return TRUE;
326 }
327 if (is_callable(array($object, 'get' . ucfirst($propertyName)))) {
328 return TRUE;
329 }
330 if (is_callable(array($object, 'has' . ucfirst($propertyName)))) {
331 return TRUE;
332 }
333 if (is_callable(array($object, 'is' . ucfirst($propertyName)))) {
334 return TRUE;
335 }
336 return array_search($propertyName, array_keys(get_class_vars(get_class($object)))) !== FALSE;
337 }
338
339 /**
340 * Get all properties (names and their current values) of the current
341 * $object that are accessible through this class.
342 *
343 * @param object $object Object to get all properties from.
344 *
345 * @throws \InvalidArgumentException
346 * @return array Associative array of all properties.
347 * @todo What to do with ArrayAccess
348 */
349 static public function getGettableProperties($object) {
350 if (!is_object($object)) {
351 throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1237301370);
352 }
353 $properties = array();
354 foreach (self::getGettablePropertyNames($object) as $propertyName) {
355 $propertyExists = FALSE;
356 $propertyValue = self::getPropertyInternal($object, $propertyName, FALSE, $propertyExists);
357 if ($propertyExists === TRUE) {
358 $properties[$propertyName] = $propertyValue;
359 }
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 */
372 static public function buildSetterMethodName($propertyName) {
373 return 'set' . ucfirst($propertyName);
374 }
375
376 }