4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\PropertyAccess
;
14 use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
;
15 use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
;
18 * Default implementation of {@link PropertyAccessorInterface}.
20 * @author Bernhard Schussek <bschussek@gmail.com>
22 class PropertyAccessor
implements PropertyAccessorInterface
30 * Should not be used by application code. Use
31 * {@link PropertyAccess::getPropertyAccessor()} instead.
33 public function __construct($magicCall = false)
35 $this->magicCall
= $magicCall;
41 public function getValue($objectOrArray, $propertyPath)
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');
49 $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength());
51 return $propertyValues[count($propertyValues) - 1][self
::VALUE
];
57 public function setValue(&$objectOrArray, $propertyPath, $value)
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');
65 $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1);
68 // Add the root object to the list
69 array_unshift($propertyValues, array(
70 self
::VALUE
=> &$objectOrArray,
74 for ($i = count($propertyValues) - 1; $i >= 0; --$i) {
75 $objectOrArray =& $propertyValues[$i][self
::VALUE
];
78 if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
79 throw new UnexpectedTypeException($objectOrArray, 'object or array');
82 $property = $propertyPath->getElement($i);
83 //$singular = $propertyPath->singulars[$i];
86 if ($propertyPath->isIndex($i)) {
87 $this->writeIndex($objectOrArray, $property, $value);
89 $this->writeProperty($objectOrArray, $property, $singular, $value);
93 $value =& $objectOrArray;
94 $overwrite = !$propertyValues[$i][self
::IS_REF
];
99 * Reads the path from an object up to a given path index.
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
105 * @return array The values read in the path.
107 * @throws UnexpectedTypeException If a value within the path is neither object nor array.
109 private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface
$propertyPath, $lastIndex)
111 $propertyValues = array();
113 for ($i = 0; $i < $lastIndex; ++
$i) {
114 if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
115 throw new UnexpectedTypeException($objectOrArray, 'object or array');
118 $property = $propertyPath->getElement($i);
119 $isIndex = $propertyPath->isIndex($i);
120 $isArrayAccess = is_array($objectOrArray) || $objectOrArray instanceof \ArrayAccess
;
122 // Create missing nested arrays on demand
123 if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) {
124 $objectOrArray[$property] = $i +
1 < $propertyPath->getLength() ? array() : null;
128 $propertyValue =& $this->readIndex($objectOrArray, $property);
130 $propertyValue =& $this->readProperty($objectOrArray, $property);
133 $objectOrArray =& $propertyValue[self
::VALUE
];
135 $propertyValues[] =& $propertyValue;
138 return $propertyValues;
142 * Reads a key from an array-like structure.
144 * @param \ArrayAccess|array $array The array or \ArrayAccess object to read from
145 * @param string|integer $index The key to read
147 * @return mixed The value of the key
149 * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array
151 private function &readIndex(&$array, $index)
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)));
157 // Use an array instead of an object since performance is very crucial here
160 self
::IS_REF
=> false
163 if (isset($array[$index])) {
164 if (is_array($array)) {
165 $result[self
::VALUE
] =& $array[$index];
166 $result[self
::IS_REF
] = true;
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;
178 * Reads the a property from an object or array.
180 * @param object $object The object to read from.
181 * @param string $property The property to read.
183 * @return mixed The value of the read property
185 * @throws NoSuchPropertyException If the property does not exist or is not
188 private function &readProperty(&$object, $property)
190 // Use an array instead of an object since performance is
194 self
::IS_REF
=> false
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));
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);
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
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();
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 '.
243 // Objects are always passed around by reference
244 if (is_object($result[self
::VALUE
])) {
245 $result[self
::IS_REF
] = true;
252 * Sets the value of the property at the given index in the path
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
258 * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array
260 private function writeIndex(&$array, $index, $value)
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)));
266 $array[$index] = $value;
270 * Sets the value of the property at the given index in the path
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
277 * @throws NoSuchPropertyException If the property does not exist or is not
280 private function writeProperty(&$object, $property, $singular, $value)
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));
288 $reflClass = new \
ReflectionClass($object);
289 $plural = $this->camelize($property);
291 // Any of the two methods is required, but not yet known
292 $singulars = null !== $singular ? array($singular) : (array) StringUtil
::singularify($plural);
294 if (is_array($value) || $value instanceof \Traversable
) {
295 $methods = $this->findAdderAndRemover($reflClass, $singulars);
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
];
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]);
313 // Next $previousItem
318 // Item not found, add to remove list
319 $itemToRemove[] = $previousItem;
323 foreach ($itemToRemove as $item) {
324 call_user_func(array($object, $methods[1]), $item);
327 foreach ($itemsToAdd as $item) {
328 call_user_func(array($object, $methods[0]), $item);
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).'()", ';
341 $setter = 'set'.$this->camelize($property);
342 $classHasProperty = $reflClass->hasProperty($property);
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
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);
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".',
373 * Camelizes a given string.
375 * @param string $string Some string
377 * @return string The camelized version of the string
379 private function camelize($string)
381 return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match
[1] ? '_' : '').strtoupper($match
[2]); }, $string);
385 * Searches for add and remove methods.
387 * @param \ReflectionClass $reflClass The reflection class for the given object
388 * @param array $singulars The singular form of the property name or null
390 * @return array|null An array containing the adder and remover when found, null otherwise
392 * @throws NoSuchPropertyException If the property does not exist
394 private function findAdderAndRemover(\ReflectionClass
$reflClass, array $singulars)
396 foreach ($singulars as $singular) {
397 $addMethod = 'add'.$singular;
398 $removeMethod = 'remove'.$singular;
400 $addMethodFound = $this->isAccessible($reflClass, $addMethod, 1);
401 $removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1);
403 if ($addMethodFound && $removeMethodFound) {
404 return array($addMethod, $removeMethod);
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,
421 * Returns whether a method is public and has a specific number of required parameters.
423 * @param \ReflectionClass $class The class of the method
424 * @param string $methodName The method name
425 * @param integer $parameters The number of parameters
427 * @return Boolean Whether the method is public and has $parameters
428 * required parameters
430 private function isAccessible(\ReflectionClass
$class, $methodName, $parameters)
432 if ($class->hasMethod($methodName)) {
433 $method = $class->getMethod($methodName);
435 if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) {