]> git.immae.eu Git - github/wallabag/wallabag.git/blob - vendor/symfony/property-access/Symfony/Component/PropertyAccess/PropertyAccessor.php
twig implementation
[github/wallabag/wallabag.git] / vendor / symfony / property-access / Symfony / Component / PropertyAccess / PropertyAccessor.php
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 }