17
17
* Prefix tree of routes preserving routes order.
18
18
*
19
19
* @author Frank de Jonge <info@frankdejonge.nl>
20
+ * @author Nicolas Grekas <p@tchwork.com>
20
21
*
21
22
* @internal
22
23
*/
23
24
class StaticPrefixCollection
24
25
{
25
26
private $ prefix ;
26
- private $ staticPrefix ;
27
- private $ matchStart = 0 ;
27
+
28
+ /**
29
+ * @var string[]
30
+ */
31
+ private $ staticPrefixes = array ();
28
32
29
33
/**
30
34
* @var string[]
@@ -36,10 +40,9 @@ class StaticPrefixCollection
36
40
*/
37
41
private $ items = array ();
38
42
39
- public function __construct (string $ prefix = '/ ' , string $ staticPrefix = ' / ' )
43
+ public function __construct (string $ prefix = '/ ' )
40
44
{
41
45
$ this ->prefix = $ prefix ;
42
- $ this ->staticPrefix = $ staticPrefix ;
43
46
}
44
47
45
48
public function getPrefix (): string
@@ -60,41 +63,38 @@ public function getRoutes(): array
60
63
*
61
64
* @param array|self $route
62
65
*/
63
- public function addRoute (string $ prefix , $ route )
66
+ public function addRoute (string $ prefix , $ route, string $ staticPrefix = null )
64
67
{
65
68
$ 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 );
77
71
}
78
72
79
- for ($ i = $ this ->matchStart ; $ i < \count ( $ this -> items ); ++ $ i ) {
73
+ for ($ i = \count ( $ this ->items ) - 1 ; 0 <= $ i ; -- $ i ) {
80
74
$ item = $ this ->items [$ i ];
81
75
82
76
if ($ item instanceof self && $ item ->accepts ($ prefix )) {
83
- $ item ->addRoute ($ prefix , $ route );
77
+ $ item ->addRoute ($ prefix , $ route, $ staticPrefix );
84
78
85
79
return ;
86
80
}
87
81
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 )) {
92
83
return ;
93
84
}
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
+ }
94
93
}
95
94
96
95
// No optimised case was found, in this case we simple add the route for possible
97
96
// grouping when new routes are added.
97
+ $ this ->staticPrefixes [] = $ staticPrefix ;
98
98
$ this ->prefixes [] = $ prefix ;
99
99
$ this ->items [] = $ route ;
100
100
}
@@ -118,41 +118,41 @@ public function populateCollection(RouteCollection $routes): RouteCollection
118
118
/**
119
119
* Tries to combine a route with another route or group.
120
120
*/
121
- private function groupWithItem (int $ i , string $ prefix , $ route ): ? self
121
+ private function groupWithItem (int $ i , string $ prefix , string $ staticPrefix , $ route ): bool
122
122
{
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 ;
125
127
}
126
128
127
- $ child = new self (...$ commonPrefix );
128
- $ item = $ this ->items [$ i ];
129
+ $ child = new self ($ commonPrefix );
129
130
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 );
136
134
137
- $ child ->addRoute ($ prefix , $ route );
135
+ $ this ->staticPrefixes [$ i ] = $ commonStaticPrefix ;
136
+ $ this ->prefixes [$ i ] = $ commonPrefix ;
137
+ $ this ->items [$ i ] = $ child ;
138
138
139
- return $ child ;
139
+ return true ;
140
140
}
141
141
142
142
/**
143
143
* Checks whether a prefix can be contained within the group.
144
144
*/
145
145
private function accepts (string $ prefix ): bool
146
146
{
147
- return '' === $ this ->prefix || 0 === strpos ($ prefix, $ this ->prefix );
147
+ return 0 === strpos ( $ prefix , $ this ->prefix ) && ' ? ' !== ($ prefix[ \strlen ( $ this ->prefix )] ?? '' );
148
148
}
149
149
150
150
/**
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 .
152
152
*
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.
154
154
*/
155
- private function detectCommonPrefix (string $ prefix , string $ anotherPrefix ): ? array
155
+ private function getCommonPrefix (string $ prefix , string $ anotherPrefix ): array
156
156
{
157
157
$ baseLength = strlen ($ this ->prefix );
158
158
$ end = min (strlen ($ prefix ), strlen ($ anotherPrefix ));
@@ -177,21 +177,23 @@ private function detectCommonPrefix(string $prefix, string $anotherPrefix): ?arr
177
177
if (0 < $ n ) {
178
178
break ;
179
179
}
180
- $ i = $ j ;
180
+ if (('? ' === ($ prefix [$ j ] ?? '' ) || '? ' === ($ anotherPrefix [$ j ] ?? '' )) && ($ prefix [$ j ] ?? '' ) !== ($ anotherPrefix [$ j ] ?? '' )) {
181
+ break ;
182
+ }
183
+ $ i = $ j - 1 ;
181
184
} elseif ('\\' === $ prefix [$ i ] && (++$ i === $ end || $ prefix [$ i ] !== $ anotherPrefix [$ i ])) {
182
185
--$ i ;
183
186
break ;
184
187
}
185
188
}
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 ;
192
194
}
193
195
194
- return null ;
196
+ return array ( substr ( $ prefix , 0 , $ i ), substr ( $ prefix , 0 , $ staticLength ?? $ i )) ;
195
197
}
196
198
197
199
/**
0 commit comments