PropertyAccessor.php 17 KB

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