PropertyPathBuilder.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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\OutOfBoundsException;
  12. /**
  13. * @author Bernhard Schussek <bschussek@gmail.com>
  14. */
  15. class PropertyPathBuilder
  16. {
  17. /**
  18. * @var array
  19. */
  20. private $elements = array();
  21. /**
  22. * @var array
  23. */
  24. private $isIndex = array();
  25. /**
  26. * Creates a new property path builder.
  27. *
  28. * @param null|PropertyPathInterface|string $path The path to initially store
  29. * in the builder. Optional.
  30. */
  31. public function __construct($path = null)
  32. {
  33. if (null !== $path) {
  34. $this->append($path);
  35. }
  36. }
  37. /**
  38. * Appends a (sub-) path to the current path.
  39. *
  40. * @param PropertyPathInterface|string $path The path to append
  41. * @param int $offset The offset where the appended
  42. * piece starts in $path.
  43. * @param int $length The length of the appended piece
  44. * If 0, the full path is appended.
  45. */
  46. public function append($path, $offset = 0, $length = 0)
  47. {
  48. if (is_string($path)) {
  49. $path = new PropertyPath($path);
  50. }
  51. if (0 === $length) {
  52. $end = $path->getLength();
  53. } else {
  54. $end = $offset + $length;
  55. }
  56. for (; $offset < $end; ++$offset) {
  57. $this->elements[] = $path->getElement($offset);
  58. $this->isIndex[] = $path->isIndex($offset);
  59. }
  60. }
  61. /**
  62. * Appends an index element to the current path.
  63. *
  64. * @param string $name The name of the appended index
  65. */
  66. public function appendIndex($name)
  67. {
  68. $this->elements[] = $name;
  69. $this->isIndex[] = true;
  70. }
  71. /**
  72. * Appends a property element to the current path.
  73. *
  74. * @param string $name The name of the appended property
  75. */
  76. public function appendProperty($name)
  77. {
  78. $this->elements[] = $name;
  79. $this->isIndex[] = false;
  80. }
  81. /**
  82. * Removes elements from the current path.
  83. *
  84. * @param int $offset The offset at which to remove
  85. * @param int $length The length of the removed piece
  86. *
  87. * @throws OutOfBoundsException if offset is invalid
  88. */
  89. public function remove($offset, $length = 1)
  90. {
  91. if (!isset($this->elements[$offset])) {
  92. throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
  93. }
  94. $this->resize($offset, $length, 0);
  95. }
  96. /**
  97. * Replaces a sub-path by a different (sub-) path.
  98. *
  99. * @param int $offset The offset at which to replace
  100. * @param int $length The length of the piece to replace
  101. * @param PropertyPathInterface|string $path The path to insert
  102. * @param int $pathOffset The offset where the inserted piece
  103. * starts in $path.
  104. * @param int $pathLength The length of the inserted piece
  105. * If 0, the full path is inserted.
  106. *
  107. * @throws OutOfBoundsException If the offset is invalid
  108. */
  109. public function replace($offset, $length, $path, $pathOffset = 0, $pathLength = 0)
  110. {
  111. if (is_string($path)) {
  112. $path = new PropertyPath($path);
  113. }
  114. if ($offset < 0 && abs($offset) <= $this->getLength()) {
  115. $offset = $this->getLength() + $offset;
  116. } elseif (!isset($this->elements[$offset])) {
  117. throw new OutOfBoundsException('The offset '.$offset.' is not within the property path');
  118. }
  119. if (0 === $pathLength) {
  120. $pathLength = $path->getLength() - $pathOffset;
  121. }
  122. $this->resize($offset, $length, $pathLength);
  123. for ($i = 0; $i < $pathLength; ++$i) {
  124. $this->elements[$offset + $i] = $path->getElement($pathOffset + $i);
  125. $this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i);
  126. }
  127. ksort($this->elements);
  128. }
  129. /**
  130. * Replaces a property element by an index element.
  131. *
  132. * @param int $offset The offset at which to replace
  133. * @param string $name The new name of the element. Optional
  134. *
  135. * @throws OutOfBoundsException If the offset is invalid
  136. */
  137. public function replaceByIndex($offset, $name = null)
  138. {
  139. if (!isset($this->elements[$offset])) {
  140. throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
  141. }
  142. if (null !== $name) {
  143. $this->elements[$offset] = $name;
  144. }
  145. $this->isIndex[$offset] = true;
  146. }
  147. /**
  148. * Replaces an index element by a property element.
  149. *
  150. * @param int $offset The offset at which to replace
  151. * @param string $name The new name of the element. Optional
  152. *
  153. * @throws OutOfBoundsException If the offset is invalid
  154. */
  155. public function replaceByProperty($offset, $name = null)
  156. {
  157. if (!isset($this->elements[$offset])) {
  158. throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
  159. }
  160. if (null !== $name) {
  161. $this->elements[$offset] = $name;
  162. }
  163. $this->isIndex[$offset] = false;
  164. }
  165. /**
  166. * Returns the length of the current path.
  167. *
  168. * @return int The path length
  169. */
  170. public function getLength()
  171. {
  172. return count($this->elements);
  173. }
  174. /**
  175. * Returns the current property path.
  176. *
  177. * @return PropertyPathInterface The constructed property path
  178. */
  179. public function getPropertyPath()
  180. {
  181. $pathAsString = $this->__toString();
  182. return '' !== $pathAsString ? new PropertyPath($pathAsString) : null;
  183. }
  184. /**
  185. * Returns the current property path as string.
  186. *
  187. * @return string The property path as string
  188. */
  189. public function __toString()
  190. {
  191. $string = '';
  192. foreach ($this->elements as $offset => $element) {
  193. if ($this->isIndex[$offset]) {
  194. $element = '['.$element.']';
  195. } elseif ('' !== $string) {
  196. $string .= '.';
  197. }
  198. $string .= $element;
  199. }
  200. return $string;
  201. }
  202. /**
  203. * Resizes the path so that a chunk of length $cutLength is
  204. * removed at $offset and another chunk of length $insertionLength
  205. * can be inserted.
  206. *
  207. * @param int $offset The offset where the removed chunk starts
  208. * @param int $cutLength The length of the removed chunk
  209. * @param int $insertionLength The length of the inserted chunk
  210. */
  211. private function resize($offset, $cutLength, $insertionLength)
  212. {
  213. // Nothing else to do in this case
  214. if ($insertionLength === $cutLength) {
  215. return;
  216. }
  217. $length = count($this->elements);
  218. if ($cutLength > $insertionLength) {
  219. // More elements should be removed than inserted
  220. $diff = $cutLength - $insertionLength;
  221. $newLength = $length - $diff;
  222. // Shift elements to the left (left-to-right until the new end)
  223. // Max allowed offset to be shifted is such that
  224. // $offset + $diff < $length (otherwise invalid index access)
  225. // i.e. $offset < $length - $diff = $newLength
  226. for ($i = $offset; $i < $newLength; ++$i) {
  227. $this->elements[$i] = $this->elements[$i + $diff];
  228. $this->isIndex[$i] = $this->isIndex[$i + $diff];
  229. }
  230. // All remaining elements should be removed
  231. for (; $i < $length; ++$i) {
  232. unset($this->elements[$i], $this->isIndex[$i]);
  233. }
  234. } else {
  235. $diff = $insertionLength - $cutLength;
  236. $newLength = $length + $diff;
  237. $indexAfterInsertion = $offset + $insertionLength;
  238. // $diff <= $insertionLength
  239. // $indexAfterInsertion >= $insertionLength
  240. // => $diff <= $indexAfterInsertion
  241. // In each of the following loops, $i >= $diff must hold,
  242. // otherwise ($i - $diff) becomes negative.
  243. // Shift old elements to the right to make up space for the
  244. // inserted elements. This needs to be done left-to-right in
  245. // order to preserve an ascending array index order
  246. // Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff,
  247. // $i >= $diff is guaranteed.
  248. for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) {
  249. $this->elements[$i] = $this->elements[$i - $diff];
  250. $this->isIndex[$i] = $this->isIndex[$i - $diff];
  251. }
  252. // Shift remaining elements to the right. Do this right-to-left
  253. // so we don't overwrite elements before copying them
  254. // The last written index is the immediate index after the inserted
  255. // string, because the indices before that will be overwritten
  256. // anyway.
  257. // Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff,
  258. // $i >= $diff is guaranteed.
  259. for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) {
  260. $this->elements[$i] = $this->elements[$i - $diff];
  261. $this->isIndex[$i] = $this->isIndex[$i - $diff];
  262. }
  263. }
  264. }
  265. }