27
27
class PhpMatcherDumper extends MatcherDumper
28
28
{
29
29
private $ expressionLanguage ;
30
+ private $ signalingException ;
30
31
31
32
/**
32
33
* @var ExpressionFunctionProviderInterface[]
@@ -87,12 +88,8 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac
87
88
88
89
/**
89
90
* Generates the code for the match method implementing UrlMatcherInterface.
90
- *
91
- * @param bool $supportsRedirections Whether redirections are supported by the base class
92
- *
93
- * @return string Match method as PHP code
94
91
*/
95
- private function generateMatchMethod ($ supportsRedirections )
92
+ private function generateMatchMethod (bool $ supportsRedirections ): string
96
93
{
97
94
// Group hosts by same-suffix, re-order when possible
98
95
$ matchHost = false ;
@@ -132,18 +129,27 @@ public function match(\$rawPathinfo)
132
129
133
130
/**
134
131
* Generates PHP code to match a RouteCollection with all its routes.
135
- *
136
- * @param RouteCollection $routes A RouteCollection instance
137
- * @param bool $supportsRedirections Whether redirections are supported by the base class
138
- *
139
- * @return string PHP code
140
132
*/
141
- private function compileRoutes (RouteCollection $ routes , $ supportsRedirections , $ matchHost )
133
+ private function compileRoutes (RouteCollection $ routes , bool $ supportsRedirections , bool $ matchHost ): string
142
134
{
143
135
list ($ staticRoutes , $ dynamicRoutes ) = $ this ->groupStaticRoutes ($ routes , $ supportsRedirections );
144
136
145
137
$ code = $ this ->compileStaticRoutes ($ staticRoutes , $ supportsRedirections , $ matchHost );
146
- $ code .= $ this ->compileDynamicRoutes ($ dynamicRoutes , $ supportsRedirections , $ matchHost );
138
+ $ chunkLimit = count ($ dynamicRoutes );
139
+
140
+ while (true ) {
141
+ try {
142
+ $ this ->signalingException = new \RuntimeException ('PCRE compilation failed: regular expression is too large ' );
143
+ $ code .= $ this ->compileDynamicRoutes ($ dynamicRoutes , $ supportsRedirections , $ matchHost , $ chunkLimit );
144
+ break ;
145
+ } catch (\Exception $ e ) {
146
+ if (1 < $ chunkLimit && $ this ->signalingException === $ e ) {
147
+ $ chunkLimit = 1 + ($ chunkLimit >> 1 );
148
+ continue ;
149
+ }
150
+ throw $ e ;
151
+ }
152
+ }
147
153
148
154
if ('' === $ code ) {
149
155
$ code .= " if ('/' === \$pathinfo) { \n" ;
@@ -275,13 +281,14 @@ private function compileStaticRoutes(array $staticRoutes, bool $supportsRedirect
275
281
* matching-but-failing subpattern is blacklisted by replacing its name by "(*F)", which forces a failure-to-match.
276
282
* To ease this backlisting operation, the name of subpatterns is also the string offset where the replacement should occur.
277
283
*/
278
- private function compileDynamicRoutes (RouteCollection $ collection , bool $ supportsRedirections , bool $ matchHost ): string
284
+ private function compileDynamicRoutes (RouteCollection $ collection , bool $ supportsRedirections , bool $ matchHost, int $ chunkLimit ): string
279
285
{
280
286
if (!$ collection ->all ()) {
281
287
return '' ;
282
288
}
283
289
$ code = '' ;
284
290
$ state = (object ) array (
291
+ 'regex ' => '' ,
285
292
'switch ' => '' ,
286
293
'default ' => '' ,
287
294
'mark ' => 0 ,
@@ -301,11 +308,13 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
301
308
return '' ;
302
309
};
303
310
311
+ $ chunkSize = 0 ;
304
312
$ prev = null ;
305
313
$ perModifiers = array ();
306
314
foreach ($ collection ->all () as $ name => $ route ) {
307
315
preg_match ('#[a-zA-Z]*$# ' , $ route ->compile ()->getRegex (), $ rx );
308
- if ($ prev !== $ rx [0 ] && $ route ->compile ()->getPathVariables ()) {
316
+ if ($ chunkLimit < ++$ chunkSize || $ prev !== $ rx [0 ] && $ route ->compile ()->getPathVariables ()) {
317
+ $ chunkSize = 1 ;
309
318
$ routes = new RouteCollection ();
310
319
$ perModifiers [] = array ($ rx [0 ], $ routes );
311
320
$ prev = $ rx [0 ];
@@ -326,8 +335,10 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
326
335
$ routes ->add ($ name , $ route );
327
336
}
328
337
$ prev = false ;
329
- $ code .= "\n {$ state ->mark } => '{^(?' " ;
330
- $ state ->mark += 4 ;
338
+ $ rx = '{^(? ' ;
339
+ $ code .= "\n {$ state ->mark } => " .self ::export ($ rx );
340
+ $ state ->mark += strlen ($ rx );
341
+ $ state ->regex = $ rx ;
331
342
332
343
foreach ($ perHost as list ($ hostRegex , $ routes )) {
333
344
if ($ matchHost ) {
@@ -340,8 +351,9 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
340
351
$ hostRegex = '[^/]*+ ' ;
341
352
$ state ->hostVars = array ();
342
353
}
343
- $ state ->mark += 3 + $ prev + strlen ($ hostRegex );
344
- $ code .= "\n . " .self ::export (($ prev ? ') ' : '' )."| {$ hostRegex }(? " );
354
+ $ state ->mark += strlen ($ rx = ($ prev ? ') ' : '' )."| {$ hostRegex }(? " );
355
+ $ code .= "\n . " .self ::export ($ rx );
356
+ $ state ->regex .= $ rx ;
345
357
$ prev = true ;
346
358
}
347
359
@@ -358,8 +370,19 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
358
370
}
359
371
if ($ matchHost ) {
360
372
$ code .= "\n .')' " ;
373
+ $ state ->regex .= ') ' ;
374
+ }
375
+ $ rx = ")$} {$ modifiers }" ;
376
+ $ code .= "\n .' {$ rx }', " ;
377
+ $ state ->regex .= $ rx ;
378
+
379
+ // if the regex is too large, throw a signaling exception to recompute with smaller chunk size
380
+ set_error_handler (function ($ type , $ message ) { throw $ this ->signalingException ; });
381
+ try {
382
+ preg_match ($ state ->regex , '' );
383
+ } finally {
384
+ restore_error_handler ();
361
385
}
362
- $ code .= "\n .')$} {$ modifiers }', " ;
363
386
}
364
387
365
388
if ($ state ->default ) {
@@ -403,7 +426,7 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
403
426
* @param \stdClass $state A simple state object that keeps track of the progress of the compilation,
404
427
* and gathers the generated switch's "case" and "default" statements
405
428
*/
406
- private function compileStaticPrefixCollection (StaticPrefixCollection $ tree , \stdClass $ state , int $ prefixLen = 0 )
429
+ private function compileStaticPrefixCollection (StaticPrefixCollection $ tree , \stdClass $ state , int $ prefixLen = 0 ): string
407
430
{
408
431
$ code = '' ;
409
432
$ prevRegex = null ;
@@ -413,10 +436,12 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
413
436
if ($ route instanceof StaticPrefixCollection) {
414
437
$ prevRegex = null ;
415
438
$ prefix = substr ($ route ->getPrefix (), $ prefixLen );
416
- $ state ->mark += 3 + strlen ($ prefix );
417
- $ code .= "\n . " .self ::export ("| {$ prefix }(? " );
439
+ $ state ->mark += strlen ($ rx = "| {$ prefix }(? " );
440
+ $ code .= "\n . " .self ::export ($ rx );
441
+ $ state ->regex .= $ rx ;
418
442
$ code .= $ this ->indent ($ this ->compileStaticPrefixCollection ($ route , $ state , $ prefixLen + strlen ($ prefix )));
419
443
$ code .= "\n .')' " ;
444
+ $ state ->regex .= ') ' ;
420
445
$ state ->markTail += 1 ;
421
446
continue ;
422
447
}
@@ -434,8 +459,9 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
434
459
$ hasTrailingSlash = $ hasTrailingSlash && (!$ methods || isset ($ methods ['GET ' ]));
435
460
$ state ->mark += 3 + $ state ->markTail + $ hasTrailingSlash + strlen ($ regex ) - $ prefixLen ;
436
461
$ state ->markTail = 2 + strlen ($ state ->mark );
437
- $ code .= "\n . " ;
438
- $ code .= self ::export (sprintf ('|%s(*:%s) ' , substr ($ regex , $ prefixLen ).($ hasTrailingSlash ? '? ' : '' ), $ state ->mark ));
462
+ $ rx = sprintf ('|%s(*:%s) ' , substr ($ regex , $ prefixLen ).($ hasTrailingSlash ? '? ' : '' ), $ state ->mark );
463
+ $ code .= "\n . " .self ::export ($ rx );
464
+ $ state ->regex .= $ rx ;
439
465
$ vars = array_merge ($ state ->hostVars , $ vars );
440
466
441
467
if (!$ route ->getCondition () && (!is_array ($ next = $ routes [1 + $ i ] ?? null ) || $ regex !== $ next [1 ])) {
@@ -472,7 +498,7 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
472
498
/**
473
499
* A simple helper to compiles the switch's "default" for both static and dynamic routes.
474
500
*/
475
- private function compileSwitchDefault (bool $ hasVars , string $ routesKey , bool $ matchHost , bool $ supportsRedirections , bool $ checkTrailingSlash )
501
+ private function compileSwitchDefault (bool $ hasVars , string $ routesKey , bool $ matchHost , bool $ supportsRedirections , bool $ checkTrailingSlash ): string
476
502
{
477
503
if ($ hasVars ) {
478
504
$ code = <<<EOF
0 commit comments