No Description

StaticPrefixCollection.php 6.7KB

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