2727class PhpMatcherDumper extends MatcherDumper
2828{
2929 private $ expressionLanguage ;
30+ private $ signalingException ;
3031
3132 /**
3233 * @var ExpressionFunctionProviderInterface[]
@@ -87,12 +88,8 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac
8788
8889 /**
8990 * 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
9491 */
95- private function generateMatchMethod ($ supportsRedirections )
92+ private function generateMatchMethod (bool $ supportsRedirections ): string
9693 {
9794 // Group hosts by same-suffix, re-order when possible
9895 $ matchHost = false ;
@@ -132,18 +129,27 @@ public function match(\$rawPathinfo)
132129
133130 /**
134131 * 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
140132 */
141- private function compileRoutes (RouteCollection $ routes , $ supportsRedirections , $ matchHost )
133+ private function compileRoutes (RouteCollection $ routes , bool $ supportsRedirections , bool $ matchHost ): string
142134 {
143135 list ($ staticRoutes , $ dynamicRoutes ) = $ this ->groupStaticRoutes ($ routes , $ supportsRedirections );
144136
145137 $ 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+ }
147153
148154 if ('' === $ code ) {
149155 $ code .= " if ('/' === \$pathinfo) { \n" ;
@@ -275,13 +281,14 @@ private function compileStaticRoutes(array $staticRoutes, bool $supportsRedirect
275281 * matching-but-failing subpattern is blacklisted by replacing its name by "(*F)", which forces a failure-to-match.
276282 * To ease this backlisting operation, the name of subpatterns is also the string offset where the replacement should occur.
277283 */
278- private function compileDynamicRoutes (RouteCollection $ collection , bool $ supportsRedirections , bool $ matchHost ): string
284+ private function compileDynamicRoutes (RouteCollection $ collection , bool $ supportsRedirections , bool $ matchHost, int $ chunkLimit ): string
279285 {
280286 if (!$ collection ->all ()) {
281287 return '' ;
282288 }
283289 $ code = '' ;
284290 $ state = (object ) array (
291+ 'regex ' => '' ,
285292 'switch ' => '' ,
286293 'default ' => '' ,
287294 'mark ' => 0 ,
@@ -301,11 +308,13 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
301308 return '' ;
302309 };
303310
311+ $ chunkSize = 0 ;
304312 $ prev = null ;
305313 $ perModifiers = array ();
306314 foreach ($ collection ->all () as $ name => $ route ) {
307315 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 ;
309318 $ routes = new RouteCollection ();
310319 $ perModifiers [] = array ($ rx [0 ], $ routes );
311320 $ prev = $ rx [0 ];
@@ -326,8 +335,10 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
326335 $ routes ->add ($ name , $ route );
327336 }
328337 $ 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 ;
331342
332343 foreach ($ perHost as list ($ hostRegex , $ routes )) {
333344 if ($ matchHost ) {
@@ -340,8 +351,9 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
340351 $ hostRegex = '[^/]*+ ' ;
341352 $ state ->hostVars = array ();
342353 }
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 ;
345357 $ prev = true ;
346358 }
347359
@@ -358,8 +370,19 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
358370 }
359371 if ($ matchHost ) {
360372 $ 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 ();
361385 }
362- $ code .= "\n .')$} {$ modifiers }', " ;
363386 }
364387
365388 if ($ state ->default ) {
@@ -403,7 +426,7 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $support
403426 * @param \stdClass $state A simple state object that keeps track of the progress of the compilation,
404427 * and gathers the generated switch's "case" and "default" statements
405428 */
406- private function compileStaticPrefixCollection (StaticPrefixCollection $ tree , \stdClass $ state , int $ prefixLen = 0 )
429+ private function compileStaticPrefixCollection (StaticPrefixCollection $ tree , \stdClass $ state , int $ prefixLen = 0 ): string
407430 {
408431 $ code = '' ;
409432 $ prevRegex = null ;
@@ -413,10 +436,12 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
413436 if ($ route instanceof StaticPrefixCollection) {
414437 $ prevRegex = null ;
415438 $ 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 ;
418442 $ code .= $ this ->indent ($ this ->compileStaticPrefixCollection ($ route , $ state , $ prefixLen + strlen ($ prefix )));
419443 $ code .= "\n .')' " ;
444+ $ state ->regex .= ') ' ;
420445 $ state ->markTail += 1 ;
421446 continue ;
422447 }
@@ -434,8 +459,9 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
434459 $ hasTrailingSlash = $ hasTrailingSlash && (!$ methods || isset ($ methods ['GET ' ]));
435460 $ state ->mark += 3 + $ state ->markTail + $ hasTrailingSlash + strlen ($ regex ) - $ prefixLen ;
436461 $ 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 ;
439465 $ vars = array_merge ($ state ->hostVars , $ vars );
440466
441467 if (!$ route ->getCondition () && (!is_array ($ next = $ routes [1 + $ i ] ?? null ) || $ regex !== $ next [1 ])) {
@@ -472,7 +498,7 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
472498 /**
473499 * A simple helper to compiles the switch's "default" for both static and dynamic routes.
474500 */
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
476502 {
477503 if ($ hasVars ) {
478504 $ code = <<<EOF
0 commit comments