PropertyPath.php 14 KB

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