PropertyPath.php 5.9 KB

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