PageRenderTime 42ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/Form/Util/PropertyPath.php

https://github.com/fernanDOTdo/symfony
PHP | 400 lines | 188 code | 48 blank | 164 comment | 51 complexity | 69321e10dca3ae703b8ce5395751077b MD5 | raw file
  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 $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. return $this->readPropertyPath($objectOrArray, 0);
  169. }
  170. /**
  171. * Sets the value at the end of the property path of the object
  172. *
  173. * Example:
  174. * <code>
  175. * $path = new PropertyPath('child.name');
  176. *
  177. * echo $path->setValue($object, 'Fabien');
  178. * // equals echo $object->getChild()->setName('Fabien');
  179. * </code>
  180. *
  181. * This method first tries to find a public setter for each property in the
  182. * path. The name of the setter must be the camel-cased property name
  183. * prefixed with "set".
  184. *
  185. * If the setter does not exist, this method tries to find a public
  186. * property. The value of the property is then changed.
  187. *
  188. * If neither is found, an exception is thrown.
  189. *
  190. * @param object|array $objectOrArray The object or array to traverse
  191. * @param mixed $value The value at the end of the
  192. * property path
  193. * @throws InvalidPropertyException If the property/setter does not
  194. * exist
  195. * @throws PropertyAccessDeniedException If the property/setter exists but
  196. * is not public
  197. */
  198. public function setValue(&$objectOrArray, $value)
  199. {
  200. $this->writePropertyPath($objectOrArray, 0, $value);
  201. }
  202. /**
  203. * Recursive implementation of getValue()
  204. *
  205. * @param object|array $objectOrArray The object or array to traverse
  206. * @param integer $currentIndex The current index in the property path
  207. * @return mixed The value at the end of the path
  208. */
  209. protected function readPropertyPath(&$objectOrArray, $currentIndex)
  210. {
  211. if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
  212. throw new UnexpectedTypeException($objectOrArray, 'object or array');
  213. }
  214. $property = $this->elements[$currentIndex];
  215. if (is_object($objectOrArray)) {
  216. $value = $this->readProperty($objectOrArray, $currentIndex);
  217. // arrays need to be treated separately (due to PHP bug?)
  218. // http://bugs.php.net/bug.php?id=52133
  219. } else {
  220. if (!array_key_exists($property, $objectOrArray)) {
  221. $objectOrArray[$property] = $currentIndex + 1 < $this->length ? array() : null;
  222. }
  223. $value =& $objectOrArray[$property];
  224. }
  225. ++$currentIndex;
  226. if ($currentIndex < $this->length) {
  227. return $this->readPropertyPath($value, $currentIndex);
  228. }
  229. return $value;
  230. }
  231. /**
  232. * Recursive implementation of setValue()
  233. *
  234. * @param object|array $objectOrArray The object or array to traverse
  235. * @param integer $currentIndex The current index in the property path
  236. * @param mixed $value The value to set at the end of the
  237. * property path
  238. */
  239. protected function writePropertyPath(&$objectOrArray, $currentIndex, $value)
  240. {
  241. if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
  242. throw new UnexpectedTypeException($objectOrArray, 'object or array');
  243. }
  244. $property = $this->elements[$currentIndex];
  245. if ($currentIndex + 1 < $this->length) {
  246. if (is_object($objectOrArray)) {
  247. $nestedObject = $this->readProperty($objectOrArray, $currentIndex);
  248. // arrays need to be treated separately (due to PHP bug?)
  249. // http://bugs.php.net/bug.php?id=52133
  250. } else {
  251. if (!array_key_exists($property, $objectOrArray)) {
  252. $objectOrArray[$property] = array();
  253. }
  254. $nestedObject =& $objectOrArray[$property];
  255. }
  256. $this->writePropertyPath($nestedObject, $currentIndex + 1, $value);
  257. } else {
  258. $this->writeProperty($objectOrArray, $currentIndex, $value);
  259. }
  260. }
  261. /**
  262. * Reads the value of the property at the given index in the path
  263. *
  264. * @param object $object The object to read from
  265. * @param integer $currentIndex The index of the read property in the path
  266. * @return mixed The value of the property
  267. */
  268. protected function readProperty($object, $currentIndex)
  269. {
  270. $property = $this->elements[$currentIndex];
  271. if ($this->isIndex[$currentIndex]) {
  272. if (!$object instanceof \ArrayAccess) {
  273. throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($object)));
  274. }
  275. return $object[$property];
  276. } else {
  277. $reflClass = new \ReflectionClass($object);
  278. $getter = 'get'.$this->camelize($property);
  279. $isser = 'is'.$this->camelize($property);
  280. if ($reflClass->hasMethod($getter)) {
  281. if (!$reflClass->getMethod($getter)->isPublic()) {
  282. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName()));
  283. }
  284. return $object->$getter();
  285. } else if ($reflClass->hasMethod($isser)) {
  286. if (!$reflClass->getMethod($isser)->isPublic()) {
  287. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName()));
  288. }
  289. return $object->$isser();
  290. } else if ($reflClass->hasMethod('__get')) {
  291. // needed to support magic method __get
  292. return $object->$property;
  293. } else if ($reflClass->hasProperty($property)) {
  294. if (!$reflClass->getProperty($property)->isPublic()) {
  295. 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)));
  296. }
  297. return $object->$property;
  298. } else if (property_exists($object, $property)) {
  299. // needed to support \stdClass instances
  300. return $object->$property;
  301. } else {
  302. throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName()));
  303. }
  304. }
  305. }
  306. /**
  307. * Sets the value of the property at the given index in the path
  308. *
  309. * @param object $objectOrArray The object or array to traverse
  310. * @param integer $currentIndex The index of the modified property in the
  311. * path
  312. * @param mixed $value The value to set
  313. */
  314. protected function writeProperty(&$objectOrArray, $currentIndex, $value)
  315. {
  316. $property = $this->elements[$currentIndex];
  317. if (is_object($objectOrArray) && $this->isIndex[$currentIndex]) {
  318. if (!$objectOrArray instanceof \ArrayAccess) {
  319. throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
  320. }
  321. $objectOrArray[$property] = $value;
  322. } else if (is_object($objectOrArray)) {
  323. $reflClass = new \ReflectionClass($objectOrArray);
  324. $setter = 'set'.$this->camelize($property);
  325. if ($reflClass->hasMethod($setter)) {
  326. if (!$reflClass->getMethod($setter)->isPublic()) {
  327. throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
  328. }
  329. $objectOrArray->$setter($value);
  330. } else if ($reflClass->hasMethod('__set')) {
  331. // needed to support magic method __set
  332. $objectOrArray->$property = $value;
  333. } else if ($reflClass->hasProperty($property)) {
  334. if (!$reflClass->getProperty($property)->isPublic()) {
  335. 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)));
  336. }
  337. $objectOrArray->$property = $value;
  338. } else if (property_exists($objectOrArray, $property)) {
  339. // needed to support \stdClass instances
  340. $objectOrArray->$property = $value;
  341. } else {
  342. throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName()));
  343. }
  344. } else {
  345. $objectOrArray[$property] = $value;
  346. }
  347. }
  348. protected function camelize($property)
  349. {
  350. return preg_replace(array('/(^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\2')", "'_'.strtoupper('\\1')"), $property);
  351. }
  352. }