PropertyPath.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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\InvalidArgumentException;
  12. use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
  13. use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException;
  14. /**
  15. * Default implementation of {@link PropertyPathInterface}.
  16. *
  17. * @author Bernhard Schussek <bschussek@gmail.com>
  18. */
  19. class PropertyPath implements \IteratorAggregate, PropertyPathInterface
  20. {
  21. /**
  22. * Character used for separating between plural and singular of an element.
  23. */
  24. public const SINGULAR_SEPARATOR = '|';
  25. /**
  26. * The elements of the property path.
  27. *
  28. * @var array
  29. */
  30. private $elements = [];
  31. /**
  32. * The number of elements in the property path.
  33. *
  34. * @var int
  35. */
  36. private $length;
  37. /**
  38. * Contains a Boolean for each property in $elements denoting whether this
  39. * element is an index. It is a property otherwise.
  40. *
  41. * @var array
  42. */
  43. private $isIndex = [];
  44. /**
  45. * String representation of the path.
  46. *
  47. * @var string
  48. */
  49. private $pathAsString;
  50. /**
  51. * Constructs a property path from a string.
  52. *
  53. * @param PropertyPath|string $propertyPath The property path as string or instance
  54. *
  55. * @throws InvalidArgumentException If the given path is not a string
  56. * @throws InvalidPropertyPathException If the syntax of the property path is not valid
  57. */
  58. public function __construct($propertyPath)
  59. {
  60. // Can be used as copy constructor
  61. if ($propertyPath instanceof self) {
  62. /* @var PropertyPath $propertyPath */
  63. $this->elements = $propertyPath->elements;
  64. $this->length = $propertyPath->length;
  65. $this->isIndex = $propertyPath->isIndex;
  66. $this->pathAsString = $propertyPath->pathAsString;
  67. return;
  68. }
  69. if (!\is_string($propertyPath)) {
  70. throw new InvalidArgumentException(sprintf('The property path constructor needs a string or an instance of "Symfony\Component\PropertyAccess\PropertyPath". Got: "%s".', get_debug_type($propertyPath)));
  71. }
  72. if ('' === $propertyPath) {
  73. throw new InvalidPropertyPathException('The property path should not be empty.');
  74. }
  75. $this->pathAsString = $propertyPath;
  76. $position = 0;
  77. $remaining = $propertyPath;
  78. // first element is evaluated differently - no leading dot for properties
  79. $pattern = '/^(([^\.\[]++)|\[([^\]]++)\])(.*)/';
  80. while (preg_match($pattern, $remaining, $matches)) {
  81. if ('' !== $matches[2]) {
  82. $element = $matches[2];
  83. $this->isIndex[] = false;
  84. } else {
  85. $element = $matches[3];
  86. $this->isIndex[] = true;
  87. }
  88. $this->elements[] = $element;
  89. $position += \strlen($matches[1]);
  90. $remaining = $matches[4];
  91. $pattern = '/^(\.([^\.|\[]++)|\[([^\]]++)\])(.*)/';
  92. }
  93. if ('' !== $remaining) {
  94. throw new InvalidPropertyPathException(sprintf('Could not parse property path "%s". Unexpected token "%s" at position %d.', $propertyPath, $remaining[0], $position));
  95. }
  96. $this->length = \count($this->elements);
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. public function __toString()
  102. {
  103. return $this->pathAsString;
  104. }
  105. /**
  106. * {@inheritdoc}
  107. */
  108. public function getLength()
  109. {
  110. return $this->length;
  111. }
  112. /**
  113. * {@inheritdoc}
  114. */
  115. public function getParent()
  116. {
  117. if ($this->length <= 1) {
  118. return null;
  119. }
  120. $parent = clone $this;
  121. --$parent->length;
  122. $parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '[')));
  123. array_pop($parent->elements);
  124. array_pop($parent->isIndex);
  125. return $parent;
  126. }
  127. /**
  128. * Returns a new iterator for this path.
  129. *
  130. * @return PropertyPathIteratorInterface
  131. */
  132. public function getIterator()
  133. {
  134. return new PropertyPathIterator($this);
  135. }
  136. /**
  137. * {@inheritdoc}
  138. */
  139. public function getElements()
  140. {
  141. return $this->elements;
  142. }
  143. /**
  144. * {@inheritdoc}
  145. */
  146. public function getElement(int $index)
  147. {
  148. if (!isset($this->elements[$index])) {
  149. throw new OutOfBoundsException(sprintf('The index "%s" is not within the property path.', $index));
  150. }
  151. return $this->elements[$index];
  152. }
  153. /**
  154. * {@inheritdoc}
  155. */
  156. public function isProperty(int $index)
  157. {
  158. if (!isset($this->isIndex[$index])) {
  159. throw new OutOfBoundsException(sprintf('The index "%s" is not within the property path.', $index));
  160. }
  161. return !$this->isIndex[$index];
  162. }
  163. /**
  164. * {@inheritdoc}
  165. */
  166. public function isIndex(int $index)
  167. {
  168. if (!isset($this->isIndex[$index])) {
  169. throw new OutOfBoundsException(sprintf('The index "%s" is not within the property path.', $index));
  170. }
  171. return $this->isIndex[$index];
  172. }
  173. }