PropertyPathBuilder.php 8.4 KB

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