PropertyPath.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien.potencier@symfony-project.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\Form;
  11. use Symfony\Component\Form\Exception\InvalidPropertyPathException;
  12. use Symfony\Component\Form\Exception\InvalidPropertyException;
  13. use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
  14. /**
  15. * Allows easy traversing of a property path
  16. *
  17. * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
  18. */
  19. class PropertyPath implements \IteratorAggregate
  20. {
  21. /**
  22. * The elements of the property path
  23. * @var array
  24. */
  25. protected $elements = array();
  26. /**
  27. * The number of elements in the property path
  28. * @var integer
  29. */
  30. protected $length;
  31. /**
  32. * Contains a Boolean for each property in $elements denoting whether this
  33. * element is an index. It is a property otherwise.
  34. * @var array
  35. */
  36. protected $isIndex = array();
  37. /**
  38. * String representation of the path
  39. * @var string
  40. */
  41. protected $string;
  42. /**
  43. * Parses the given property path
  44. *
  45. * @param string $this
  46. */
  47. public function __construct($propertyPath)
  48. {
  49. if ('' === $propertyPath || null === $propertyPath) {
  50. throw new InvalidPropertyPathException('The property path must not be empty');
  51. }
  52. $this->string = $propertyPath;
  53. $position = 0;
  54. $remaining = $propertyPath;
  55. // first element is evaluated differently - no leading dot for properties
  56. $pattern = '/^((\w+)|\[(\w+)\])(.*)/';
  57. while (preg_match($pattern, $remaining, $matches)) {
  58. if ($matches[2] !== '') {
  59. $this->elements[] = $matches[2];
  60. $this->isIndex[] = false;
  61. } else {
  62. $this->elements[] = $matches[3];
  63. $this->isIndex[] = true;
  64. }
  65. $position += strlen($matches[1]);
  66. $remaining = $matches[4];
  67. $pattern = '/^(\.(\w+)|\[(\w+)\])(.*)/';
  68. }
  69. if (!empty($remaining)) {
  70. throw new InvalidPropertyPathException(sprintf(
  71. 'Could not parse property path "%s". Unexpected token "%s" at position %d',
  72. $propertyPath,
  73. $remaining{0},
  74. $position
  75. ));
  76. }
  77. $this->length = count($this->elements);
  78. }
  79. /**
  80. * Returns the string representation of the property path
  81. *
  82. * @return string
  83. */
  84. public function __toString()
  85. {
  86. return $this->string;
  87. }
  88. /**
  89. * Returns a new iterator for this path
  90. *
  91. * @return PropertyPathIterator
  92. */
  93. public function getIterator()
  94. {
  95. return new PropertyPathIterator($this);
  96. }
  97. /**
  98. * Returns the elements of the property path as array
  99. *
  100. * @return array An array of property/index names
  101. */
  102. public function getElements()
  103. {
  104. return $this->elements;
  105. }
  106. /**
  107. * Returns the element at the given index in the property path
  108. *
  109. * @return string A property or index name
  110. */
  111. public function getElement($index)
  112. {
  113. return $this->elements[$index];
  114. }
  115. /**
  116. * Returns whether the element at the given index is a property
  117. *
  118. * @param integer $index The index in the property path
  119. * @return Boolean Whether the element at this index is a property
  120. */
  121. public function isProperty($index)
  122. {
  123. return !$this->isIndex($index);
  124. }
  125. /**
  126. * Returns whether the element at the given index is an array index
  127. *
  128. * @param integer $index The index in the property path
  129. * @return Boolean Whether the element at this index is an array index
  130. */
  131. public function isIndex($index)
  132. {
  133. return $this->isIndex[$index];
  134. }
  135. /**
  136. * Returns the value at the end of the property path of the object
  137. *
  138. * Example:
  139. * <code>
  140. * $path = new PropertyPath('child.name');
  141. *
  142. * echo $path->getValue($object);
  143. * // equals echo $object->getChild()->getName();
  144. * </code>
  145. *
  146. * This method first tries to find a public getter for each property in the
  147. * path. The name of the getter must be the camel-cased property name
  148. * prefixed with "get" or "is".
  149. *
  150. * If the getter does not exist, this method tries to find a public
  151. * property. The value of the property is then returned.
  152. *
  153. * If neither is found, an exception is thrown.
  154. *
  155. * @param object|array $objectOrArray The object or array to traverse
  156. * @return mixed The value at the end of the
  157. * property path
  158. * @throws InvalidPropertyException If the property/getter does not
  159. * exist
  160. * @throws PropertyAccessDeniedException If the property/getter exists but
  161. * is not public
  162. */
  163. public function getValue($objectOrArray)
  164. {
  165. return $this->readPropertyPath($objectOrArray, 0);
  166. }
  167. /**
  168. * Sets the value at the end of the property path of the object
  169. *
  170. * Example:
  171. * <code>
  172. * $path = new PropertyPath('child.name');
  173. *
  174. * echo $path->setValue($object, 'Fabien');
  175. * // equals echo $object->getChild()->setName('Fabien');
  176. * </code>
  177. *
  178. * This method first tries to find a public setter for each property in the
  179. * path. The name of the setter must be the camel-cased property name
  180. * prefixed with "set".
  181. *
  182. * If the setter does not exist, this method tries to find a public
  183. * property. The value of the property is then changed.
  184. *
  185. * If neither is found, an exception is thrown.
  186. *
  187. * @param object|array $objectOrArray The object or array to traverse
  188. * @return mixed The value at the end of the
  189. * property path
  190. * @throws InvalidPropertyException If the property/setter does not
  191. * exist
  192. * @throws PropertyAccessDeniedException If the property/setter exists but
  193. * is not public
  194. */
  195. public function setValue(&$objectOrArray, $value)
  196. {
  197. $this->updatePropertyPath($objectOrArray, 0, $value);
  198. }
  199. /**
  200. * Recursive implementation of getValue()
  201. *
  202. * @param object|array $objectOrArray The object or array to traverse
  203. * @param integer $currentIndex The current index in the property path
  204. * @return mixed The value at the end of the path
  205. */
  206. protected function readPropertyPath(&$objectOrArray, $currentIndex)
  207. {
  208. $property = $this->elements[$currentIndex];
  209. if (is_object($objectOrArray)) {
  210. $value = $this->readProperty($objectOrArray, $currentIndex);
  211. // arrays need to be treated separately (due to PHP bug?)
  212. // http://bugs.php.net/bug.php?id=52133
  213. } else {
  214. if (!array_key_exists($property, $objectOrArray)) {
  215. $objectOrArray[$property] = $currentIndex + 1 < $this->length ? array() : null;
  216. }
  217. $value =& $objectOrArray[$property];
  218. }
  219. ++$currentIndex;
  220. if ($currentIndex < $this->length) {
  221. return $this->readPropertyPath($value, $currentIndex);
  222. } else {
  223. return $value;
  224. }
  225. }
  226. /**
  227. * Recursive implementation of setValue()
  228. *
  229. * @param object|array $objectOrArray The object or array to traverse
  230. * @param integer $currentIndex The current index in the property path
  231. * @param mixed $value The value to set at the end of the
  232. * property path
  233. */
  234. protected function updatePropertyPath(&$objectOrArray, $currentIndex, $value)
  235. {
  236. $property = $this->elements[$currentIndex];
  237. if ($currentIndex + 1 < $this->length) {
  238. if (is_object($objectOrArray)) {
  239. $nestedObject = $this->readProperty($objectOrArray, $currentIndex);
  240. // arrays need to be treated separately (due to PHP bug?)
  241. // http://bugs.php.net/bug.php?id=52133
  242. } else {
  243. if (!array_key_exists($property, $objectOrArray)) {
  244. $objectOrArray[$property] = array();
  245. }
  246. $nestedObject =& $objectOrArray[$property];
  247. }
  248. $this->updatePropertyPath($nestedObject, $currentIndex + 1, $value);
  249. } else {
  250. $this->updateProperty($objectOrArray, $currentIndex, $value);
  251. }
  252. }
  253. /**
  254. * Reads the value of the property at the given index in the path
  255. *
  256. * @param object $object The object to read from
  257. * @param integer $currentIndex The index of the read property in the path
  258. * @return mixed The value of the property
  259. */
  260. protected function readProperty($object, $currentIndex)
  261. {
  262. $property = $this->elements[$currentIndex];
  263. if ($this->isIndex[$currentIndex]) {
  264. if (!$object instanceof \ArrayAccess) {
  265. throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($object)));
  266. }
  267. return $object[$property];
  268. } else {
  269. $reflClass = new \ReflectionClass($object);
  270. $getter = 'get'.$this->camelize($property);
  271. $isser = 'is'.$this->camelize($property);
  272. if ($reflClass->hasMethod($getter)) {
  273. if (!$reflClass->getMethod($getter)->isPublic()) {
  274. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName()));
  275. }
  276. return $object->$getter();
  277. } else if ($reflClass->hasMethod($isser)) {
  278. if (!$reflClass->getMethod($isser)->isPublic()) {
  279. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName()));
  280. }
  281. return $object->$isser();
  282. } else if ($reflClass->hasMethod('__get')) {
  283. // needed to support magic method __get
  284. return $object->$property;
  285. } else if ($reflClass->hasProperty($property)) {
  286. if (!$reflClass->getProperty($property)->isPublic()) {
  287. throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "get%s()" or "is%s()"?', $property, $reflClass->getName(), ucfirst($property), ucfirst($property)));
  288. }
  289. return $object->$property;
  290. } else if (property_exists($object, $property)) {
  291. // needed to support \stdClass instances
  292. return $object->$property;
  293. } else {
  294. throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName()));
  295. }
  296. }
  297. }
  298. /**
  299. * Sets the value of the property at the given index in the path
  300. *
  301. * @param object $object The object or array to traverse
  302. * @param integer $currentIndex The index of the modified property in the
  303. * path
  304. * @param mixed $value The value to set
  305. */
  306. protected function updateProperty(&$objectOrArray, $currentIndex, $value)
  307. {
  308. $property = $this->elements[$currentIndex];
  309. if (is_object($objectOrArray) && $this->isIndex[$currentIndex]) {
  310. if (!$objectOrArray instanceof \ArrayAccess) {
  311. throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
  312. }
  313. $objectOrArray[$property] = $value;
  314. } else if (is_object($objectOrArray)) {
  315. $reflClass = new \ReflectionClass($objectOrArray);
  316. $setter = 'set'.$this->camelize($property);
  317. if ($reflClass->hasMethod($setter)) {
  318. if (!$reflClass->getMethod($setter)->isPublic()) {
  319. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
  320. }
  321. $objectOrArray->$setter($value);
  322. } else if ($reflClass->hasMethod('__set')) {
  323. // needed to support magic method __set
  324. $objectOrArray->$property = $value;
  325. } else if ($reflClass->hasProperty($property)) {
  326. if (!$reflClass->getProperty($property)->isPublic()) {
  327. throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?', $property, $reflClass->getName(), ucfirst($property)));
  328. }
  329. $objectOrArray->$property = $value;
  330. } else if (property_exists($objectOrArray, $property)) {
  331. // needed to support \stdClass instances
  332. $objectOrArray->$property = $value;
  333. } else {
  334. throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName()));
  335. }
  336. } else {
  337. $objectOrArray[$property] = $value;
  338. }
  339. }
  340. protected function camelize($property)
  341. {
  342. return preg_replace(array('/(^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\2')", "'_'.strtoupper('\\1')"), $property);
  343. }
  344. }