StaticPrefixCollection.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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\Routing\Matcher\Dumper;
  11. use Symfony\Component\Routing\RouteCollection;
  12. /**
  13. * Prefix tree of routes preserving routes order.
  14. *
  15. * @author Frank de Jonge <info@frankdejonge.nl>
  16. * @author Nicolas Grekas <p@tchwork.com>
  17. *
  18. * @internal
  19. */
  20. class StaticPrefixCollection
  21. {
  22. /**
  23. * @var string[]
  24. */
  25. private array $staticPrefixes = [];
  26. /**
  27. * @var string[]
  28. */
  29. private array $prefixes = [];
  30. /**
  31. * @var array[]|self[]
  32. */
  33. private array $items = [];
  34. public function __construct(
  35. private string $prefix = '/',
  36. ) {
  37. }
  38. public function getPrefix(): string
  39. {
  40. return $this->prefix;
  41. }
  42. /**
  43. * @return array[]|self[]
  44. */
  45. public function getRoutes(): array
  46. {
  47. return $this->items;
  48. }
  49. /**
  50. * Adds a route to a group.
  51. */
  52. public function addRoute(string $prefix, array|self $route): void
  53. {
  54. [$prefix, $staticPrefix] = $this->getCommonPrefix($prefix, $prefix);
  55. for ($i = \count($this->items) - 1; 0 <= $i; --$i) {
  56. $item = $this->items[$i];
  57. [$commonPrefix, $commonStaticPrefix] = $this->getCommonPrefix($prefix, $this->prefixes[$i]);
  58. if ($this->prefix === $commonPrefix) {
  59. // the new route and a previous one have no common prefix, let's see if they are exclusive to each others
  60. if ($this->prefix !== $staticPrefix && $this->prefix !== $this->staticPrefixes[$i]) {
  61. // the new route and the previous one have exclusive static prefixes
  62. continue;
  63. }
  64. if ($this->prefix === $staticPrefix && $this->prefix === $this->staticPrefixes[$i]) {
  65. // the new route and the previous one have no static prefix
  66. break;
  67. }
  68. if ($this->prefixes[$i] !== $this->staticPrefixes[$i] && $this->prefix === $this->staticPrefixes[$i]) {
  69. // the previous route is non-static and has no static prefix
  70. break;
  71. }
  72. if ($prefix !== $staticPrefix && $this->prefix === $staticPrefix) {
  73. // the new route is non-static and has no static prefix
  74. break;
  75. }
  76. continue;
  77. }
  78. if ($item instanceof self && $this->prefixes[$i] === $commonPrefix) {
  79. // the new route is a child of a previous one, let's nest it
  80. $item->addRoute($prefix, $route);
  81. } else {
  82. // the new route and a previous one have a common prefix, let's merge them
  83. $child = new self($commonPrefix);
  84. [$child->prefixes[0], $child->staticPrefixes[0]] = $child->getCommonPrefix($this->prefixes[$i], $this->prefixes[$i]);
  85. [$child->prefixes[1], $child->staticPrefixes[1]] = $child->getCommonPrefix($prefix, $prefix);
  86. $child->items = [$this->items[$i], $route];
  87. $this->staticPrefixes[$i] = $commonStaticPrefix;
  88. $this->prefixes[$i] = $commonPrefix;
  89. $this->items[$i] = $child;
  90. }
  91. return;
  92. }
  93. // No optimised case was found, in this case we simple add the route for possible
  94. // grouping when new routes are added.
  95. $this->staticPrefixes[] = $staticPrefix;
  96. $this->prefixes[] = $prefix;
  97. $this->items[] = $route;
  98. }
  99. /**
  100. * Linearizes back a set of nested routes into a collection.
  101. */
  102. public function populateCollection(RouteCollection $routes): RouteCollection
  103. {
  104. foreach ($this->items as $route) {
  105. if ($route instanceof self) {
  106. $route->populateCollection($routes);
  107. } else {
  108. $routes->add(...$route);
  109. }
  110. }
  111. return $routes;
  112. }
  113. /**
  114. * Gets the full and static common prefixes between two route patterns.
  115. *
  116. * The static prefix stops at last at the first opening bracket.
  117. */
  118. private function getCommonPrefix(string $prefix, string $anotherPrefix): array
  119. {
  120. $baseLength = \strlen($this->prefix);
  121. $end = min(\strlen($prefix), \strlen($anotherPrefix));
  122. $staticLength = null;
  123. set_error_handler(self::handleError(...));
  124. try {
  125. for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) {
  126. if ('(' === $prefix[$i]) {
  127. $staticLength ??= $i;
  128. for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) {
  129. if ($prefix[$j] !== $anotherPrefix[$j]) {
  130. break 2;
  131. }
  132. if ('(' === $prefix[$j]) {
  133. ++$n;
  134. } elseif (')' === $prefix[$j]) {
  135. --$n;
  136. } elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) {
  137. --$j;
  138. break;
  139. }
  140. }
  141. if (0 < $n) {
  142. break;
  143. }
  144. if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) {
  145. break;
  146. }
  147. $subPattern = substr($prefix, $i, $j - $i);
  148. if ($prefix !== $anotherPrefix && !preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !preg_match('{(?<!'.$subPattern.')}', '')) {
  149. // sub-patterns of variable length are not considered as common prefixes because their greediness would break in-order matching
  150. break;
  151. }
  152. $i = $j - 1;
  153. } elseif ('\\' === $prefix[$i] && (++$i === $end || $prefix[$i] !== $anotherPrefix[$i])) {
  154. --$i;
  155. break;
  156. }
  157. }
  158. } finally {
  159. restore_error_handler();
  160. }
  161. if ($i < $end && 0b10 === (\ord($prefix[$i]) >> 6) && preg_match('//u', $prefix.' '.$anotherPrefix)) {
  162. do {
  163. // Prevent cutting in the middle of an UTF-8 characters
  164. --$i;
  165. } while (0b10 === (\ord($prefix[$i]) >> 6));
  166. }
  167. return [substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i)];
  168. }
  169. public static function handleError(int $type, string $msg): bool
  170. {
  171. return str_contains($msg, 'Compilation failed: lookbehind assertion is not fixed length')
  172. || str_contains($msg, 'Compilation failed: length of lookbehind assertion is not limited');
  173. }
  174. }