]>
Commit | Line | Data |
---|---|---|
4f5b44bd NL |
1 | <?php |
2 | ||
3 | /* | |
4 | * This file is part of the Symfony package. | |
5 | * | |
6 | * (c) Fabien Potencier <fabien@symfony.com> | |
7 | * | |
8 | * For the full copyright and license information, please view the LICENSE | |
9 | * file that was distributed with this source code. | |
10 | */ | |
11 | ||
12 | namespace Symfony\Component\PropertyAccess; | |
13 | ||
14 | use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; | |
15 | use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; | |
16 | ||
17 | /** | |
18 | * Default implementation of {@link PropertyAccessorInterface}. | |
19 | * | |
20 | * @author Bernhard Schussek <bschussek@gmail.com> | |
21 | */ | |
22 | class PropertyAccessor implements PropertyAccessorInterface | |
23 | { | |
24 | const VALUE = 0; | |
25 | const IS_REF = 1; | |
26 | ||
27 | private $magicCall; | |
28 | ||
29 | /** | |
30 | * Should not be used by application code. Use | |
31 | * {@link PropertyAccess::getPropertyAccessor()} instead. | |
32 | */ | |
33 | public function __construct($magicCall = false) | |
34 | { | |
35 | $this->magicCall = $magicCall; | |
36 | } | |
37 | ||
38 | /** | |
39 | * {@inheritdoc} | |
40 | */ | |
41 | public function getValue($objectOrArray, $propertyPath) | |
42 | { | |
43 | if (is_string($propertyPath)) { | |
44 | $propertyPath = new PropertyPath($propertyPath); | |
45 | } elseif (!$propertyPath instanceof PropertyPathInterface) { | |
46 | throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); | |
47 | } | |
48 | ||
49 | $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength()); | |
50 | ||
51 | return $propertyValues[count($propertyValues) - 1][self::VALUE]; | |
52 | } | |
53 | ||
54 | /** | |
55 | * {@inheritdoc} | |
56 | */ | |
57 | public function setValue(&$objectOrArray, $propertyPath, $value) | |
58 | { | |
59 | if (is_string($propertyPath)) { | |
60 | $propertyPath = new PropertyPath($propertyPath); | |
61 | } elseif (!$propertyPath instanceof PropertyPathInterface) { | |
62 | throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); | |
63 | } | |
64 | ||
65 | $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); | |
66 | $overwrite = true; | |
67 | ||
68 | // Add the root object to the list | |
69 | array_unshift($propertyValues, array( | |
70 | self::VALUE => &$objectOrArray, | |
71 | self::IS_REF => true, | |
72 | )); | |
73 | ||
74 | for ($i = count($propertyValues) - 1; $i >= 0; --$i) { | |
75 | $objectOrArray =& $propertyValues[$i][self::VALUE]; | |
76 | ||
77 | if ($overwrite) { | |
78 | if (!is_object($objectOrArray) && !is_array($objectOrArray)) { | |
79 | throw new UnexpectedTypeException($objectOrArray, 'object or array'); | |
80 | } | |
81 | ||
82 | $property = $propertyPath->getElement($i); | |
83 | //$singular = $propertyPath->singulars[$i]; | |
84 | $singular = null; | |
85 | ||
86 | if ($propertyPath->isIndex($i)) { | |
87 | $this->writeIndex($objectOrArray, $property, $value); | |
88 | } else { | |
89 | $this->writeProperty($objectOrArray, $property, $singular, $value); | |
90 | } | |
91 | } | |
92 | ||
93 | $value =& $objectOrArray; | |
94 | $overwrite = !$propertyValues[$i][self::IS_REF]; | |
95 | } | |
96 | } | |
97 | ||
98 | /** | |
99 | * Reads the path from an object up to a given path index. | |
100 | * | |
101 | * @param object|array $objectOrArray The object or array to read from | |
102 | * @param PropertyPathInterface $propertyPath The property path to read | |
103 | * @param integer $lastIndex The index up to which should be read | |
104 | * | |
105 | * @return array The values read in the path. | |
106 | * | |
107 | * @throws UnexpectedTypeException If a value within the path is neither object nor array. | |
108 | */ | |
109 | private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex) | |
110 | { | |
111 | $propertyValues = array(); | |
112 | ||
113 | for ($i = 0; $i < $lastIndex; ++$i) { | |
114 | if (!is_object($objectOrArray) && !is_array($objectOrArray)) { | |
115 | throw new UnexpectedTypeException($objectOrArray, 'object or array'); | |
116 | } | |
117 | ||
118 | $property = $propertyPath->getElement($i); | |
119 | $isIndex = $propertyPath->isIndex($i); | |
120 | $isArrayAccess = is_array($objectOrArray) || $objectOrArray instanceof \ArrayAccess; | |
121 | ||
122 | // Create missing nested arrays on demand | |
123 | if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) { | |
124 | $objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null; | |
125 | } | |
126 | ||
127 | if ($isIndex) { | |
128 | $propertyValue =& $this->readIndex($objectOrArray, $property); | |
129 | } else { | |
130 | $propertyValue =& $this->readProperty($objectOrArray, $property); | |
131 | } | |
132 | ||
133 | $objectOrArray =& $propertyValue[self::VALUE]; | |
134 | ||
135 | $propertyValues[] =& $propertyValue; | |
136 | } | |
137 | ||
138 | return $propertyValues; | |
139 | } | |
140 | ||
141 | /** | |
142 | * Reads a key from an array-like structure. | |
143 | * | |
144 | * @param \ArrayAccess|array $array The array or \ArrayAccess object to read from | |
145 | * @param string|integer $index The key to read | |
146 | * | |
147 | * @return mixed The value of the key | |
148 | * | |
149 | * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array | |
150 | */ | |
151 | private function &readIndex(&$array, $index) | |
152 | { | |
153 | if (!$array instanceof \ArrayAccess && !is_array($array)) { | |
154 | throw new NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); | |
155 | } | |
156 | ||
157 | // Use an array instead of an object since performance is very crucial here | |
158 | $result = array( | |
159 | self::VALUE => null, | |
160 | self::IS_REF => false | |
161 | ); | |
162 | ||
163 | if (isset($array[$index])) { | |
164 | if (is_array($array)) { | |
165 | $result[self::VALUE] =& $array[$index]; | |
166 | $result[self::IS_REF] = true; | |
167 | } else { | |
168 | $result[self::VALUE] = $array[$index]; | |
169 | // Objects are always passed around by reference | |
170 | $result[self::IS_REF] = is_object($array[$index]) ? true : false; | |
171 | } | |
172 | } | |
173 | ||
174 | return $result; | |
175 | } | |
176 | ||
177 | /** | |
178 | * Reads the a property from an object or array. | |
179 | * | |
180 | * @param object $object The object to read from. | |
181 | * @param string $property The property to read. | |
182 | * | |
183 | * @return mixed The value of the read property | |
184 | * | |
185 | * @throws NoSuchPropertyException If the property does not exist or is not | |
186 | * public. | |
187 | */ | |
188 | private function &readProperty(&$object, $property) | |
189 | { | |
190 | // Use an array instead of an object since performance is | |
191 | // very crucial here | |
192 | $result = array( | |
193 | self::VALUE => null, | |
194 | self::IS_REF => false | |
195 | ); | |
196 | ||
197 | if (!is_object($object)) { | |
198 | throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); | |
199 | } | |
200 | ||
201 | $camelProp = $this->camelize($property); | |
202 | $reflClass = new \ReflectionClass($object); | |
203 | $getter = 'get'.$camelProp; | |
204 | $isser = 'is'.$camelProp; | |
205 | $hasser = 'has'.$camelProp; | |
206 | $classHasProperty = $reflClass->hasProperty($property); | |
207 | ||
208 | if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { | |
209 | $result[self::VALUE] = $object->$getter(); | |
210 | } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) { | |
211 | $result[self::VALUE] = $object->$isser(); | |
212 | } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) { | |
213 | $result[self::VALUE] = $object->$hasser(); | |
214 | } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) { | |
215 | $result[self::VALUE] = $object->$property; | |
216 | } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { | |
217 | $result[self::VALUE] =& $object->$property; | |
218 | $result[self::IS_REF] = true; | |
219 | } elseif (!$classHasProperty && property_exists($object, $property)) { | |
220 | // Needed to support \stdClass instances. We need to explicitly | |
221 | // exclude $classHasProperty, otherwise if in the previous clause | |
222 | // a *protected* property was found on the class, property_exists() | |
223 | // returns true, consequently the following line will result in a | |
224 | // fatal error. | |
225 | $result[self::VALUE] =& $object->$property; | |
226 | $result[self::IS_REF] = true; | |
227 | } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { | |
228 | // we call the getter and hope the __call do the job | |
229 | $result[self::VALUE] = $object->$getter(); | |
230 | } else { | |
231 | throw new NoSuchPropertyException(sprintf( | |
232 | 'Neither the property "%s" nor one of the methods "%s()", '. | |
233 | '"%s()", "%s()", "__get()" or "__call()" exist and have public access in '. | |
234 | 'class "%s".', | |
235 | $property, | |
236 | $getter, | |
237 | $isser, | |
238 | $hasser, | |
239 | $reflClass->name | |
240 | )); | |
241 | } | |
242 | ||
243 | // Objects are always passed around by reference | |
244 | if (is_object($result[self::VALUE])) { | |
245 | $result[self::IS_REF] = true; | |
246 | } | |
247 | ||
248 | return $result; | |
249 | } | |
250 | ||
251 | /** | |
252 | * Sets the value of the property at the given index in the path | |
253 | * | |
254 | * @param \ArrayAccess|array $array An array or \ArrayAccess object to write to | |
255 | * @param string|integer $index The index to write at | |
256 | * @param mixed $value The value to write | |
257 | * | |
258 | * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array | |
259 | */ | |
260 | private function writeIndex(&$array, $index, $value) | |
261 | { | |
262 | if (!$array instanceof \ArrayAccess && !is_array($array)) { | |
263 | throw new NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); | |
264 | } | |
265 | ||
266 | $array[$index] = $value; | |
267 | } | |
268 | ||
269 | /** | |
270 | * Sets the value of the property at the given index in the path | |
271 | * | |
272 | * @param object|array $object The object or array to write to | |
273 | * @param string $property The property to write | |
274 | * @param string|null $singular The singular form of the property name or null | |
275 | * @param mixed $value The value to write | |
276 | * | |
277 | * @throws NoSuchPropertyException If the property does not exist or is not | |
278 | * public. | |
279 | */ | |
280 | private function writeProperty(&$object, $property, $singular, $value) | |
281 | { | |
282 | $guessedAdders = ''; | |
283 | ||
284 | if (!is_object($object)) { | |
285 | throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); | |
286 | } | |
287 | ||
288 | $reflClass = new \ReflectionClass($object); | |
289 | $plural = $this->camelize($property); | |
290 | ||
291 | // Any of the two methods is required, but not yet known | |
292 | $singulars = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural); | |
293 | ||
294 | if (is_array($value) || $value instanceof \Traversable) { | |
295 | $methods = $this->findAdderAndRemover($reflClass, $singulars); | |
296 | ||
297 | if (null !== $methods) { | |
298 | // At this point the add and remove methods have been found | |
299 | // Use iterator_to_array() instead of clone in order to prevent side effects | |
300 | // see https://github.com/symfony/symfony/issues/4670 | |
301 | $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value; | |
302 | $itemToRemove = array(); | |
303 | $propertyValue = $this->readProperty($object, $property); | |
304 | $previousValue = $propertyValue[self::VALUE]; | |
305 | ||
306 | if (is_array($previousValue) || $previousValue instanceof \Traversable) { | |
307 | foreach ($previousValue as $previousItem) { | |
308 | foreach ($value as $key => $item) { | |
309 | if ($item === $previousItem) { | |
310 | // Item found, don't add | |
311 | unset($itemsToAdd[$key]); | |
312 | ||
313 | // Next $previousItem | |
314 | continue 2; | |
315 | } | |
316 | } | |
317 | ||
318 | // Item not found, add to remove list | |
319 | $itemToRemove[] = $previousItem; | |
320 | } | |
321 | } | |
322 | ||
323 | foreach ($itemToRemove as $item) { | |
324 | call_user_func(array($object, $methods[1]), $item); | |
325 | } | |
326 | ||
327 | foreach ($itemsToAdd as $item) { | |
328 | call_user_func(array($object, $methods[0]), $item); | |
329 | } | |
330 | ||
331 | return; | |
332 | } else { | |
333 | // It is sufficient to include only the adders in the error | |
334 | // message. If the user implements the adder but not the remover, | |
335 | // an exception will be thrown in findAdderAndRemover() that | |
336 | // the remover has to be implemented as well. | |
337 | $guessedAdders = '"add'.implode('()", "add', $singulars).'()", '; | |
338 | } | |
339 | } | |
340 | ||
341 | $setter = 'set'.$this->camelize($property); | |
342 | $classHasProperty = $reflClass->hasProperty($property); | |
343 | ||
344 | if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) { | |
345 | $object->$setter($value); | |
346 | } elseif ($reflClass->hasMethod('__set') && $reflClass->getMethod('__set')->isPublic()) { | |
347 | $object->$property = $value; | |
348 | } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { | |
349 | $object->$property = $value; | |
350 | } elseif (!$classHasProperty && property_exists($object, $property)) { | |
351 | // Needed to support \stdClass instances. We need to explicitly | |
352 | // exclude $classHasProperty, otherwise if in the previous clause | |
353 | // a *protected* property was found on the class, property_exists() | |
354 | // returns true, consequently the following line will result in a | |
355 | // fatal error. | |
356 | $object->$property = $value; | |
357 | } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { | |
358 | // we call the getter and hope the __call do the job | |
359 | $object->$setter($value); | |
360 | } else { | |
361 | throw new NoSuchPropertyException(sprintf( | |
362 | 'Neither the property "%s" nor one of the methods %s"%s()", '. | |
363 | '"__set()" or "__call()" exist and have public access in class "%s".', | |
364 | $property, | |
365 | $guessedAdders, | |
366 | $setter, | |
367 | $reflClass->name | |
368 | )); | |
369 | } | |
370 | } | |
371 | ||
372 | /** | |
373 | * Camelizes a given string. | |
374 | * | |
375 | * @param string $string Some string | |
376 | * | |
377 | * @return string The camelized version of the string | |
378 | */ | |
379 | private function camelize($string) | |
380 | { | |
381 | return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $string); | |
382 | } | |
383 | ||
384 | /** | |
385 | * Searches for add and remove methods. | |
386 | * | |
387 | * @param \ReflectionClass $reflClass The reflection class for the given object | |
388 | * @param array $singulars The singular form of the property name or null | |
389 | * | |
390 | * @return array|null An array containing the adder and remover when found, null otherwise | |
391 | * | |
392 | * @throws NoSuchPropertyException If the property does not exist | |
393 | */ | |
394 | private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) | |
395 | { | |
396 | foreach ($singulars as $singular) { | |
397 | $addMethod = 'add'.$singular; | |
398 | $removeMethod = 'remove'.$singular; | |
399 | ||
400 | $addMethodFound = $this->isAccessible($reflClass, $addMethod, 1); | |
401 | $removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1); | |
402 | ||
403 | if ($addMethodFound && $removeMethodFound) { | |
404 | return array($addMethod, $removeMethod); | |
405 | } | |
406 | ||
407 | if ($addMethodFound xor $removeMethodFound) { | |
408 | throw new NoSuchPropertyException(sprintf( | |
409 | 'Found the public method "%s()", but did not find a public "%s()" on class %s', | |
410 | $addMethodFound ? $addMethod : $removeMethod, | |
411 | $addMethodFound ? $removeMethod : $addMethod, | |
412 | $reflClass->name | |
413 | )); | |
414 | } | |
415 | } | |
416 | ||
417 | return null; | |
418 | } | |
419 | ||
420 | /** | |
421 | * Returns whether a method is public and has a specific number of required parameters. | |
422 | * | |
423 | * @param \ReflectionClass $class The class of the method | |
424 | * @param string $methodName The method name | |
425 | * @param integer $parameters The number of parameters | |
426 | * | |
427 | * @return Boolean Whether the method is public and has $parameters | |
428 | * required parameters | |
429 | */ | |
430 | private function isAccessible(\ReflectionClass $class, $methodName, $parameters) | |
431 | { | |
432 | if ($class->hasMethod($methodName)) { | |
433 | $method = $class->getMethod($methodName); | |
434 | ||
435 | if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) { | |
436 | return true; | |
437 | } | |
438 | } | |
439 | ||
440 | return false; | |
441 | } | |
442 | } |