1717 * Prefix tree of routes preserving routes order.
1818 *
1919 * @author Frank de Jonge <info@frankdejonge.nl>
20+ * @author Nicolas Grekas <p@tchwork.com>
2021 *
2122 * @internal
2223 */
2324class StaticPrefixCollection
2425{
2526 private $ prefix ;
26- private $ staticPrefix ;
27- private $ matchStart = 0 ;
27+
28+ /**
29+ * @var string[]
30+ */
31+ private $ staticPrefixes = array ();
2832
2933 /**
3034 * @var string[]
@@ -36,10 +40,9 @@ class StaticPrefixCollection
3640 */
3741 private $ items = array ();
3842
39- public function __construct (string $ prefix = '/ ' , string $ staticPrefix = ' / ' )
43+ public function __construct (string $ prefix = '/ ' )
4044 {
4145 $ this ->prefix = $ prefix ;
42- $ this ->staticPrefix = $ staticPrefix ;
4346 }
4447
4548 public function getPrefix (): string
@@ -60,41 +63,38 @@ public function getRoutes(): array
6063 *
6164 * @param array|self $route
6265 */
63- public function addRoute (string $ prefix , $ route )
66+ public function addRoute (string $ prefix , $ route, string $ staticPrefix = null )
6467 {
6568 $ this ->guardAgainstAddingNotAcceptedRoutes ($ prefix );
66- list ($ prefix , $ staticPrefix ) = $ this ->detectCommonPrefix ($ prefix , $ prefix ) ?: array (rtrim ($ prefix , '/ ' ) ?: '/ ' , '/ ' );
67-
68- if ($ this ->staticPrefix === $ staticPrefix ) {
69- // When a prefix is exactly the same as the base we move up the match start position.
70- // This is needed because otherwise routes that come afterwards have higher precedence
71- // than a possible regular expression, which goes against the input order sorting.
72- $ this ->prefixes [] = $ prefix ;
73- $ this ->items [] = $ route ;
74- $ this ->matchStart = count ($ this ->items );
75-
76- return ;
69+ if (null === $ staticPrefix ) {
70+ list ($ prefix , $ staticPrefix ) = $ this ->getCommonPrefix ($ prefix , $ prefix );
7771 }
7872
79- for ($ i = $ this ->matchStart ; $ i < \count ( $ this -> items ); ++ $ i ) {
73+ for ($ i = \count ( $ this ->items ) - 1 ; 0 <= $ i ; -- $ i ) {
8074 $ item = $ this ->items [$ i ];
8175
8276 if ($ item instanceof self && $ item ->accepts ($ prefix )) {
83- $ item ->addRoute ($ prefix , $ route );
77+ $ item ->addRoute ($ prefix , $ route, $ staticPrefix );
8478
8579 return ;
8680 }
8781
88- if ($ group = $ this ->groupWithItem ($ i , $ prefix , $ route )) {
89- $ this ->prefixes [$ i ] = $ group ->getPrefix ();
90- $ this ->items [$ i ] = $ group ;
91-
82+ if ($ this ->groupWithItem ($ i , $ prefix , $ staticPrefix , $ route )) {
9283 return ;
9384 }
85+
86+ if ($ this ->staticPrefixes [$ i ] !== $ this ->prefixes [$ i ] && 0 === strpos ($ staticPrefix , $ this ->staticPrefixes [$ i ])) {
87+ break ;
88+ }
89+
90+ if ($ staticPrefix !== $ prefix && 0 === strpos ($ this ->staticPrefixes [$ i ], $ staticPrefix )) {
91+ break ;
92+ }
9493 }
9594
9695 // No optimised case was found, in this case we simple add the route for possible
9796 // grouping when new routes are added.
97+ $ this ->staticPrefixes [] = $ staticPrefix ;
9898 $ this ->prefixes [] = $ prefix ;
9999 $ this ->items [] = $ route ;
100100 }
@@ -118,44 +118,44 @@ public function populateCollection(RouteCollection $routes): RouteCollection
118118 /**
119119 * Tries to combine a route with another route or group.
120120 */
121- private function groupWithItem (int $ i , string $ prefix , $ route ): ? self
121+ private function groupWithItem (int $ i , string $ prefix , string $ staticPrefix , $ route ): bool
122122 {
123- if (!$ commonPrefix = $ this ->detectCommonPrefix ($ prefix , $ this ->prefixes [$ i ])) {
124- return null ;
123+ list ($ commonPrefix , $ commonStaticPrefix ) = $ this ->getCommonPrefix ($ prefix , $ this ->prefixes [$ i ]);
124+
125+ if (\strlen ($ this ->prefix ) >= \strlen ($ commonPrefix )) {
126+ return false ;
125127 }
126128
127- $ child = new self (...$ commonPrefix );
128- $ item = $ this ->items [$ i ];
129+ $ child = new self ($ commonPrefix );
129130
130- if ($ item instanceof self) {
131- $ child ->prefixes = array ($ commonPrefix [0 ]);
132- $ child ->items = array ($ item );
133- } else {
134- $ child ->addRoute ($ this ->prefixes [$ i ], $ item );
135- }
131+ $ child ->staticPrefixes = array ($ this ->staticPrefixes [$ i ], $ staticPrefix );
132+ $ child ->prefixes = array ($ this ->prefixes [$ i ], $ prefix );
133+ $ child ->items = array ($ this ->items [$ i ], $ route );
136134
137- $ child ->addRoute ($ prefix , $ route );
135+ $ this ->staticPrefixes [$ i ] = $ commonStaticPrefix ;
136+ $ this ->prefixes [$ i ] = $ commonPrefix ;
137+ $ this ->items [$ i ] = $ child ;
138138
139- return $ child ;
139+ return true ;
140140 }
141141
142142 /**
143143 * Checks whether a prefix can be contained within the group.
144144 */
145145 private function accepts (string $ prefix ): bool
146146 {
147- return '' === $ this ->prefix || 0 === strpos ($ prefix, $ this ->prefix );
147+ return 0 === strpos ( $ prefix , $ this ->prefix ) && ' ? ' !== ($ prefix[ \strlen ( $ this ->prefix )] ?? '' );
148148 }
149149
150150 /**
151- * Detects whether there's a common prefix relative to the group prefix and returns it .
151+ * Gets the full and static common prefixes between two route patterns .
152152 *
153- * @return null|array A common prefix, longer than the base/group prefix, or null when none available
153+ * The static prefix stops at last at the first opening bracket.
154154 */
155- private function detectCommonPrefix (string $ prefix , string $ anotherPrefix ): ? array
155+ private function getCommonPrefix (string $ prefix , string $ anotherPrefix ): array
156156 {
157- $ baseLength = strlen ($ this ->prefix );
158- $ end = min (strlen ($ prefix ), strlen ($ anotherPrefix ));
157+ $ baseLength = \ strlen ($ this ->prefix );
158+ $ end = min (\ strlen ($ prefix ), \ strlen ($ anotherPrefix ));
159159 $ staticLength = null ;
160160
161161 for ($ i = $ baseLength ; $ i < $ end && $ prefix [$ i ] === $ anotherPrefix [$ i ]; ++$ i ) {
@@ -177,21 +177,23 @@ private function detectCommonPrefix(string $prefix, string $anotherPrefix): ?arr
177177 if (0 < $ n ) {
178178 break ;
179179 }
180- $ i = $ j ;
180+ if (('? ' === ($ prefix [$ j ] ?? '' ) || '? ' === ($ anotherPrefix [$ j ] ?? '' )) && ($ prefix [$ j ] ?? '' ) !== ($ anotherPrefix [$ j ] ?? '' )) {
181+ break ;
182+ }
183+ $ i = $ j - 1 ;
181184 } elseif ('\\' === $ prefix [$ i ] && (++$ i === $ end || $ prefix [$ i ] !== $ anotherPrefix [$ i ])) {
182185 --$ i ;
183186 break ;
184187 }
185188 }
186-
187- $ staticLength = $ staticLength ?? $ i ;
188- $ commonPrefix = rtrim (substr ($ prefix , 0 , $ i ), '/ ' );
189-
190- if (strlen ($ commonPrefix ) > $ baseLength ) {
191- return array ($ commonPrefix , rtrim (substr ($ prefix , 0 , $ staticLength ), '/ ' ) ?: '/ ' );
189+ if (1 < $ i && '/ ' === $ prefix [$ i - 1 ]) {
190+ --$ i ;
191+ }
192+ if (null !== $ staticLength && 1 < $ staticLength && '/ ' === $ prefix [$ staticLength - 1 ]) {
193+ --$ staticLength ;
192194 }
193195
194- return null ;
196+ return array ( substr ( $ prefix , 0 , $ i ), substr ( $ prefix , 0 , $ staticLength ?? $ i )) ;
195197 }
196198
197199 /**
0 commit comments