PropertyPath.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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 $propertyPath
  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. * @param integer $index The index key
  111. *
  112. * @return string A property or index name
  113. */
  114. public function getElement($index)
  115. {
  116. return $this->elements[$index];
  117. }
  118. /**
  119. * Returns whether the element at the given index is a property
  120. *
  121. * @param integer $index The index in the property path
  122. * @return Boolean Whether the element at this index is a property
  123. */
  124. public function isProperty($index)
  125. {
  126. return !$this->isIndex[$index];
  127. }
  128. /**
  129. * Returns whether the element at the given index is an array index
  130. *
  131. * @param integer $index The index in the property path
  132. * @return Boolean Whether the element at this index is an array index
  133. */
  134. public function isIndex($index)
  135. {
  136. return $this->isIndex[$index];
  137. }
  138. /**
  139. * Returns the value at the end of the property path of the object
  140. *
  141. * Example:
  142. * <code>
  143. * $path = new PropertyPath('child.name');
  144. *
  145. * echo $path->getValue($object);
  146. * // equals echo $object->getChild()->getName();
  147. * </code>
  148. *
  149. * This method first tries to find a public getter for each property in the
  150. * path. The name of the getter must be the camel-cased property name
  151. * prefixed with "get" or "is".
  152. *
  153. * If the getter does not exist, this method tries to find a public
  154. * property. The value of the property is then returned.
  155. *
  156. * If neither is found, an exception is thrown.
  157. *
  158. * @param object|array $objectOrArray The object or array to traverse
  159. * @return mixed The value at the end of the
  160. * property path
  161. * @throws InvalidPropertyException If the property/getter does not
  162. * exist
  163. * @throws PropertyAccessDeniedException If the property/getter exists but
  164. * is not public
  165. */
  166. public function getValue($objectOrArray)
  167. {
  168. for ($i = 0; $i < $this->length; ++$i) {
  169. if (is_object($objectOrArray)) {
  170. $value = $this->readProperty($objectOrArray, $i);
  171. // arrays need to be treated separately (due to PHP bug?)
  172. // http://bugs.php.net/bug.php?id=52133
  173. } else if (is_array($objectOrArray)){
  174. $property = $this->elements[$i];
  175. if (!array_key_exists($property, $objectOrArray)) {
  176. $objectOrArray[$property] = $i + 1 < $this->length ? array() : null;
  177. }
  178. $value =& $objectOrArray[$property];
  179. } else {
  180. throw new UnexpectedTypeException($objectOrArray, 'object or array');
  181. }
  182. $objectOrArray =& $value;
  183. }
  184. return $value;
  185. }
  186. /**
  187. * Sets the value at the end of the property path of the object
  188. *
  189. * Example:
  190. * <code>
  191. * $path = new PropertyPath('child.name');
  192. *
  193. * echo $path->setValue($object, 'Fabien');
  194. * // equals echo $object->getChild()->setName('Fabien');
  195. * </code>
  196. *
  197. * This method first tries to find a public setter for each property in the
  198. * path. The name of the setter must be the camel-cased property name
  199. * prefixed with "set".
  200. *
  201. * If the setter does not exist, this method tries to find a public
  202. * property. The value of the property is then changed.
  203. *
  204. * If neither is found, an exception is thrown.
  205. *
  206. * @param object|array $objectOrArray The object or array to traverse
  207. * @param mixed $value The value at the end of the
  208. * property path
  209. * @throws InvalidPropertyException If the property/setter does not
  210. * exist
  211. * @throws PropertyAccessDeniedException If the property/setter exists but
  212. * is not public
  213. */
  214. public function setValue(&$objectOrArray, $value)
  215. {
  216. for ($i = 0, $l = $this->length - 1; $i < $l; ++$i) {
  217. if (is_object($objectOrArray)) {
  218. $nestedObject = $this->readProperty($objectOrArray, $i);
  219. // arrays need to be treated separately (due to PHP bug?)
  220. // http://bugs.php.net/bug.php?id=52133
  221. } else if (is_array($objectOrArray)) {
  222. $property = $this->elements[$i];
  223. if (!array_key_exists($property, $objectOrArray)) {
  224. $objectOrArray[$property] = array();
  225. }
  226. $nestedObject =& $objectOrArray[$property];
  227. } else {
  228. throw new UnexpectedTypeException($objectOrArray, 'object or array');
  229. }
  230. $objectOrArray =& $nestedObject;
  231. }
  232. if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
  233. throw new UnexpectedTypeException($objectOrArray, 'object or array');
  234. }
  235. $this->writeProperty($objectOrArray, $i, $value);
  236. }
  237. /**
  238. * Reads the value of the property at the given index in the path
  239. *
  240. * @param object $object The object to read from
  241. * @param integer $currentIndex The index of the read property in the path
  242. * @return mixed The value of the property
  243. */
  244. protected function readProperty($object, $currentIndex)
  245. {
  246. $property = $this->elements[$currentIndex];
  247. if ($this->isIndex[$currentIndex]) {
  248. if (!$object instanceof \ArrayAccess) {
  249. throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($object)));
  250. }
  251. return $object[$property];
  252. } else {
  253. $camelProp = $this->camelize($property);
  254. $reflClass = new \ReflectionClass($object);
  255. $getter = 'get'.$camelProp;
  256. $isser = 'is'.$camelProp;
  257. if ($reflClass->hasMethod($getter)) {
  258. if (!$reflClass->getMethod($getter)->isPublic()) {
  259. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName()));
  260. }
  261. return $object->$getter();
  262. } else if ($reflClass->hasMethod($isser)) {
  263. if (!$reflClass->getMethod($isser)->isPublic()) {
  264. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName()));
  265. }
  266. return $object->$isser();
  267. } else if ($reflClass->hasMethod('__get')) {
  268. // needed to support magic method __get
  269. return $object->$property;
  270. } else if ($reflClass->hasProperty($property)) {
  271. if (!$reflClass->getProperty($property)->isPublic()) {
  272. throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()"?', $property, $reflClass->getName(), $getter, $isser));
  273. }
  274. return $object->$property;
  275. } else if (property_exists($object, $property)) {
  276. // needed to support \stdClass instances
  277. return $object->$property;
  278. } else {
  279. throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName()));
  280. }
  281. }
  282. }
  283. /**
  284. * Sets the value of the property at the given index in the path
  285. *
  286. * @param object $objectOrArray The object or array to traverse
  287. * @param integer $currentIndex The index of the modified property in the
  288. * path
  289. * @param mixed $value The value to set
  290. */
  291. protected function writeProperty(&$objectOrArray, $currentIndex, $value)
  292. {
  293. $property = $this->elements[$currentIndex];
  294. if (is_object($objectOrArray) && $this->isIndex[$currentIndex]) {
  295. if (!$objectOrArray instanceof \ArrayAccess) {
  296. throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
  297. }
  298. $objectOrArray[$property] = $value;
  299. } else if (is_object($objectOrArray)) {
  300. $reflClass = new \ReflectionClass($objectOrArray);
  301. $setter = 'set'.$this->camelize($property);
  302. if ($reflClass->hasMethod($setter)) {
  303. if (!$reflClass->getMethod($setter)->isPublic()) {
  304. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
  305. }
  306. $objectOrArray->$setter($value);
  307. } else if ($reflClass->hasMethod('__set')) {
  308. // needed to support magic method __set
  309. $objectOrArray->$property = $value;
  310. } else if ($reflClass->hasProperty($property)) {
  311. if (!$reflClass->getProperty($property)->isPublic()) {
  312. throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()"?', $property, $reflClass->getName(), $setter));
  313. }
  314. $objectOrArray->$property = $value;
  315. } else if (property_exists($objectOrArray, $property)) {
  316. // needed to support \stdClass instances
  317. $objectOrArray->$property = $value;
  318. } else {
  319. throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName()));
  320. }
  321. } else {
  322. $objectOrArray[$property] = $value;
  323. }
  324. }
  325. protected function camelize($property)
  326. {
  327. return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $property);
  328. }
  329. }