StaticPrefixCollection.php 7.0 KB

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