4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\PropertyAccess
;
14 use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException
;
17 * @author Bernhard Schussek <bschussek@gmail.com>
19 class PropertyPathBuilder
24 private $elements = array();
29 private $isIndex = array();
32 * Creates a new property path builder.
34 * @param null|PropertyPathInterface|string $path The path to initially store
35 * in the builder. Optional.
37 public function __construct($path = null)
45 * Appends a (sub-) path to the current path.
47 * @param PropertyPathInterface|string $path The path to append.
48 * @param integer $offset The offset where the appended
49 * piece starts in $path.
50 * @param integer $length The length of the appended piece.
51 * If 0, the full path is appended.
53 public function append($path, $offset = 0, $length = 0)
55 if (is_string($path)) {
56 $path = new PropertyPath($path);
60 $end = $path->getLength();
62 $end = $offset +
$length;
65 for (; $offset < $end; ++
$offset) {
66 $this->elements
[] = $path->getElement($offset);
67 $this->isIndex
[] = $path->isIndex($offset);
72 * Appends an index element to the current path.
74 * @param string $name The name of the appended index
76 public function appendIndex($name)
78 $this->elements
[] = $name;
79 $this->isIndex
[] = true;
83 * Appends a property element to the current path.
85 * @param string $name The name of the appended property
87 public function appendProperty($name)
89 $this->elements
[] = $name;
90 $this->isIndex
[] = false;
94 * Removes elements from the current path.
96 * @param integer $offset The offset at which to remove
97 * @param integer $length The length of the removed piece
99 * @throws OutOfBoundsException if offset is invalid
101 public function remove($offset, $length = 1)
103 if (!isset($this->elements
[$offset])) {
104 throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
107 $this->resize($offset, $length, 0);
111 * Replaces a sub-path by a different (sub-) path.
113 * @param integer $offset The offset at which to replace.
114 * @param integer $length The length of the piece to replace.
115 * @param PropertyPathInterface|string $path The path to insert.
116 * @param integer $pathOffset The offset where the inserted piece
118 * @param integer $pathLength The length of the inserted piece.
119 * If 0, the full path is inserted.
121 * @throws OutOfBoundsException If the offset is invalid
123 public function replace($offset, $length, $path, $pathOffset = 0, $pathLength = 0)
125 if (is_string($path)) {
126 $path = new PropertyPath($path);
129 if ($offset < 0 && abs($offset) <= $this->getLength()) {
130 $offset = $this->getLength() +
$offset;
131 } elseif (!isset($this->elements
[$offset])) {
132 throw new OutOfBoundsException('The offset ' . $offset . ' is not within the property path');
135 if (0 === $pathLength) {
136 $pathLength = $path->getLength() - $pathOffset;
139 $this->resize($offset, $length, $pathLength);
141 for ($i = 0; $i < $pathLength; ++
$i) {
142 $this->elements
[$offset +
$i] = $path->getElement($pathOffset +
$i);
143 $this->isIndex
[$offset +
$i] = $path->isIndex($pathOffset +
$i);
148 * Replaces a property element by an index element.
150 * @param integer $offset The offset at which to replace
151 * @param string $name The new name of the element. Optional.
153 * @throws OutOfBoundsException If the offset is invalid
155 public function replaceByIndex($offset, $name = null)
157 if (!isset($this->elements
[$offset])) {
158 throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
161 if (null !== $name) {
162 $this->elements
[$offset] = $name;
165 $this->isIndex
[$offset] = true;
169 * Replaces an index element by a property element.
171 * @param integer $offset The offset at which to replace
172 * @param string $name The new name of the element. Optional.
174 * @throws OutOfBoundsException If the offset is invalid
176 public function replaceByProperty($offset, $name = null)
178 if (!isset($this->elements
[$offset])) {
179 throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
182 if (null !== $name) {
183 $this->elements
[$offset] = $name;
186 $this->isIndex
[$offset] = false;
190 * Returns the length of the current path.
192 * @return integer The path length
194 public function getLength()
196 return count($this->elements
);
200 * Returns the current property path.
202 * @return PropertyPathInterface The constructed property path
204 public function getPropertyPath()
206 $pathAsString = $this->__toString();
208 return '' !== $pathAsString ? new PropertyPath($pathAsString) : null;
212 * Returns the current property path as string.
214 * @return string The property path as string
216 public function __toString()
220 foreach ($this->elements
as $offset => $element) {
221 if ($this->isIndex
[$offset]) {
222 $element = '['.$element.']';
223 } elseif ('' !== $string) {
234 * Resizes the path so that a chunk of length $cutLength is
235 * removed at $offset and another chunk of length $insertionLength
238 * @param integer $offset The offset where the removed chunk starts
239 * @param integer $cutLength The length of the removed chunk
240 * @param integer $insertionLength The length of the inserted chunk
242 private function resize($offset, $cutLength, $insertionLength)
244 // Nothing else to do in this case
245 if ($insertionLength === $cutLength) {
249 $length = count($this->elements
);
251 if ($cutLength > $insertionLength) {
252 // More elements should be removed than inserted
253 $diff = $cutLength - $insertionLength;
254 $newLength = $length - $diff;
256 // Shift elements to the left (left-to-right until the new end)
257 // Max allowed offset to be shifted is such that
258 // $offset + $diff < $length (otherwise invalid index access)
259 // i.e. $offset < $length - $diff = $newLength
260 for ($i = $offset; $i < $newLength; ++
$i) {
261 $this->elements
[$i] = $this->elements
[$i +
$diff];
262 $this->isIndex
[$i] = $this->isIndex
[$i +
$diff];
265 // All remaining elements should be removed
266 for (; $i < $length; ++
$i) {
267 unset($this->elements
[$i]);
268 unset($this->isIndex
[$i]);
271 $diff = $insertionLength - $cutLength;
273 $newLength = $length +
$diff;
274 $indexAfterInsertion = $offset +
$insertionLength;
276 // $diff <= $insertionLength
277 // $indexAfterInsertion >= $insertionLength
278 // => $diff <= $indexAfterInsertion
280 // In each of the following loops, $i >= $diff must hold,
281 // otherwise ($i - $diff) becomes negative.
283 // Shift old elements to the right to make up space for the
284 // inserted elements. This needs to be done left-to-right in
285 // order to preserve an ascending array index order
286 // Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff,
287 // $i >= $diff is guaranteed.
288 for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++
$i) {
289 $this->elements
[$i] = $this->elements
[$i - $diff];
290 $this->isIndex
[$i] = $this->isIndex
[$i - $diff];
293 // Shift remaining elements to the right. Do this right-to-left
294 // so we don't overwrite elements before copying them
295 // The last written index is the immediate index after the inserted
296 // string, because the indices before that will be overwritten
298 // Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff,
299 // $i >= $diff is guaranteed.
300 for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) {
301 $this->elements
[$i] = $this->elements
[$i - $diff];
302 $this->isIndex
[$i] = $this->isIndex
[$i - $diff];