-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Routing] Optimised dumped router matcher, prevent unneeded function calls. #21755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
ea8c55a
0d69cfe
0425f33
56f2323
ef6ad3a
44271e0
e4a9e1e
2e180c6
9d92c57
f56cf9a
bb68154
4ab821b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -105,8 +105,16 @@ public function match(\$pathinfo) | |
{ | ||
\$allow = array(); | ||
\$pathinfo = rawurldecode(\$pathinfo); | ||
\$trimmedPathinfo = rtrim(\$pathinfo, '/'); | ||
\$context = \$this->context; | ||
\$request = \$this->request; | ||
\$requestMethod = \$isLikeGetMethod = \$context->getMethod(); | ||
\$schema = \$context->getScheme(); | ||
|
||
if ('HEAD' === \$requestMethod) { | ||
\$isLikeGetMethod = 'GET'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The We had some discussions about what a "safe method" is in HTTP, but the section 9.1.1 of RFC 2616 mentions Or at least, rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, we don't want a boolean, because then we can't perform a meaningful optimisation. We just want to have a interpreted method, which we can check against. Otherwise we'd need to do more check than less. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about renaming it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've renamed the variable to |
||
} | ||
|
||
|
||
$code | ||
|
||
|
@@ -133,7 +141,7 @@ private function compileRoutes(RouteCollection $routes, $supportsRedirections) | |
foreach ($groups as $collection) { | ||
if (null !== $regex = $collection->getAttribute('host_regex')) { | ||
if (!$fetchedHost) { | ||
$code .= " \$host = \$this->context->getHost();\n\n"; | ||
$code .= " \$host = \$context->getHost();\n\n"; | ||
$fetchedHost = true; | ||
} | ||
|
||
|
@@ -217,20 +225,15 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren | |
$hostMatches = false; | ||
$methods = $route->getMethods(); | ||
|
||
// GET and HEAD are equivalent | ||
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) { | ||
$methods[] = 'HEAD'; | ||
} | ||
|
||
$supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods)); | ||
$regex = $compiledRoute->getRegex(); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This removed N trim calls. |
||
if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#'.(substr($regex, -1) === 'u' ? 'u' : ''), $regex, $m)) { | ||
if ($supportsTrailingSlash && substr($m['url'], -1) === '/') { | ||
$conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true)); | ||
$conditions[] = sprintf('%s === $trimmedPathinfo', var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true)); | ||
$hasTrailingSlash = true; | ||
} else { | ||
$conditions[] = sprintf('$pathinfo === %s', var_export(str_replace('\\', '', $m['url']), true)); | ||
$conditions[] = sprintf('%s === $pathinfo', var_export(str_replace('\\', '', $m['url']), true)); | ||
} | ||
} else { | ||
if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) { | ||
|
@@ -263,26 +266,57 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren | |
EOF; | ||
|
||
$gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This removes N function calls. |
||
if ($methods) { | ||
if (1 === count($methods)) { | ||
$code .= <<<EOF | ||
if (\$this->context->getMethod() != '$methods[0]') { | ||
if ($methods[0] === 'HEAD') { | ||
$code .= <<<EOF | ||
if ('HEAD' !== \$requestMethod) { | ||
\$allow[] = 'HEAD'; | ||
goto $gotoname; | ||
} | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This removes N function calls. |
||
EOF; | ||
} else { | ||
$code .= <<<EOF | ||
if ('$methods[0]' !== \$isLikeGetMethod) { | ||
\$allow[] = '$methods[0]'; | ||
goto $gotoname; | ||
} | ||
|
||
|
||
EOF; | ||
} | ||
} else { | ||
$methods = implode("', '", $methods); | ||
$code .= <<<EOF | ||
if (!in_array(\$this->context->getMethod(), array('$methods'))) { | ||
$methodVariable = 'requestMethod'; | ||
|
||
if (in_array('GET', $methods)) { | ||
// Since we treat HEAD requests like GET requests we don't need to match it. | ||
$methodVariable = 'isLikeGetMethod'; | ||
$methods = array_filter($methods, function ($method) { return 'HEAD' !== $method; }); | ||
} | ||
|
||
if (1 === count($methods)) { | ||
$code .= <<<EOF | ||
if ('$methods[0]' !== \$$methodVariable) { | ||
\$allow[] = '$methods[0]'; | ||
goto $gotoname; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Has the use of the goto ever been discussed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm checking this out now, if proven to be effective I'll create another PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @GuilhemN I suspected it to have some effect, but it did literally nothing. |
||
} | ||
|
||
|
||
EOF; | ||
} else { | ||
$methods = implode("', '", $methods); | ||
$code .= <<<EOF | ||
if (!in_array(\$$methodVariable, array('$methods'))) { | ||
\$allow = array_merge(\$allow, array('$methods')); | ||
goto $gotoname; | ||
} | ||
|
||
|
||
EOF; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -303,7 +337,7 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren | |
$schemes = str_replace("\n", '', var_export(array_flip($schemes), true)); | ||
$code .= <<<EOF | ||
\$requiredSchemes = $schemes; | ||
if (!isset(\$requiredSchemes[\$this->context->getScheme()])) { | ||
if (!isset(\$requiredSchemes[\$schema])) { | ||
return \$this->redirect(\$pathinfo, '$name', key(\$requiredSchemes)); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,8 +24,16 @@ public function match($pathinfo) | |
{ | ||
$allow = array(); | ||
$pathinfo = rawurldecode($pathinfo); | ||
$trimmedPathinfo = rtrim($pathinfo, '/'); | ||
$context = $this->context; | ||
$request = $this->request; | ||
$requestMethod = $isLikeGetMethod = $context->getMethod(); | ||
$schema = $context->getScheme(); | ||
|
||
if ('HEAD' === $requestMethod) { | ||
$isLikeGetMethod = 'GET'; | ||
} | ||
|
||
|
||
// foo | ||
if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) { | ||
|
@@ -35,8 +43,8 @@ public function match($pathinfo) | |
if (0 === strpos($pathinfo, '/bar')) { | ||
// bar | ||
if (preg_match('#^/bar/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) { | ||
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) { | ||
$allow = array_merge($allow, array('GET', 'HEAD')); | ||
if ('GET' !== $isLikeGetMethod) { | ||
$allow[] = 'GET'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why aren't you adding HEAD anymore here ? It is an allowed method There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stof this was an optimisation proposed by @fabpot. We stream HEAD in a special way (this is current behaviour). When a HEAD request is made, we also match GET. Inverted, when we declare GET actions, we also match in HEAD. So in this normalised case we know that if something is "like get" we don't need HEAD anymore because we're we already match GET. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make sure that this is properly tested? If someone else breaks this, it will be a huge bug! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Jean85 there's an additional test case added in this PR to do just that. |
||
goto not_bar; | ||
} | ||
|
||
|
@@ -46,8 +54,8 @@ public function match($pathinfo) | |
|
||
// barhead | ||
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) { | ||
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) { | ||
$allow = array_merge($allow, array('GET', 'HEAD')); | ||
if ('GET' !== $isLikeGetMethod) { | ||
$allow[] = 'GET'; | ||
goto not_barhead; | ||
} | ||
|
||
|
@@ -60,17 +68,17 @@ public function match($pathinfo) | |
if (0 === strpos($pathinfo, '/test')) { | ||
if (0 === strpos($pathinfo, '/test/baz')) { | ||
// baz | ||
if ($pathinfo === '/test/baz') { | ||
if ('/test/baz' === $pathinfo) { | ||
return array('_route' => 'baz'); | ||
} | ||
|
||
// baz2 | ||
if ($pathinfo === '/test/baz.html') { | ||
if ('/test/baz.html' === $pathinfo) { | ||
return array('_route' => 'baz2'); | ||
} | ||
|
||
// baz3 | ||
if ($pathinfo === '/test/baz3/') { | ||
if ('/test/baz3/' === $pathinfo) { | ||
return array('_route' => 'baz3'); | ||
} | ||
|
||
|
@@ -83,7 +91,7 @@ public function match($pathinfo) | |
|
||
// baz5 | ||
if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) { | ||
if ($this->context->getMethod() != 'POST') { | ||
if ('POST' !== $isLikeGetMethod) { | ||
$allow[] = 'POST'; | ||
goto not_baz5; | ||
} | ||
|
@@ -94,7 +102,7 @@ public function match($pathinfo) | |
|
||
// baz.baz6 | ||
if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) { | ||
if ($this->context->getMethod() != 'PUT') { | ||
if ('PUT' !== $isLikeGetMethod) { | ||
$allow[] = 'PUT'; | ||
goto not_bazbaz6; | ||
} | ||
|
@@ -106,7 +114,7 @@ public function match($pathinfo) | |
} | ||
|
||
// foofoo | ||
if ($pathinfo === '/foofoo') { | ||
if ('/foofoo' === $pathinfo) { | ||
return array ( 'def' => 'test', '_route' => 'foofoo',); | ||
} | ||
|
||
|
@@ -116,7 +124,7 @@ public function match($pathinfo) | |
} | ||
|
||
// space | ||
if ($pathinfo === '/spa ce') { | ||
if ('/spa ce' === $pathinfo) { | ||
return array('_route' => 'space'); | ||
} | ||
|
||
|
@@ -161,12 +169,12 @@ public function match($pathinfo) | |
} | ||
|
||
// overridden2 | ||
if ($pathinfo === '/multi/new') { | ||
if ('/multi/new' === $pathinfo) { | ||
return array('_route' => 'overridden2'); | ||
} | ||
|
||
// hey | ||
if ($pathinfo === '/multi/hey/') { | ||
if ('/multi/hey/' === $pathinfo) { | ||
return array('_route' => 'hey'); | ||
} | ||
|
||
|
@@ -184,7 +192,7 @@ public function match($pathinfo) | |
|
||
if (0 === strpos($pathinfo, '/aba')) { | ||
// ababa | ||
if ($pathinfo === '/ababa') { | ||
if ('/ababa' === $pathinfo) { | ||
return array('_route' => 'ababa'); | ||
} | ||
|
||
|
@@ -195,59 +203,59 @@ public function match($pathinfo) | |
|
||
} | ||
|
||
$host = $this->context->getHost(); | ||
$host = $context->getHost(); | ||
|
||
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) { | ||
// route1 | ||
if ($pathinfo === '/route1') { | ||
if ('/route1' === $pathinfo) { | ||
return array('_route' => 'route1'); | ||
} | ||
|
||
// route2 | ||
if ($pathinfo === '/c2/route2') { | ||
if ('/c2/route2' === $pathinfo) { | ||
return array('_route' => 'route2'); | ||
} | ||
|
||
} | ||
|
||
if (preg_match('#^b\\.example\\.com$#si', $host, $hostMatches)) { | ||
// route3 | ||
if ($pathinfo === '/c2/route3') { | ||
if ('/c2/route3' === $pathinfo) { | ||
return array('_route' => 'route3'); | ||
} | ||
|
||
} | ||
|
||
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) { | ||
// route4 | ||
if ($pathinfo === '/route4') { | ||
if ('/route4' === $pathinfo) { | ||
return array('_route' => 'route4'); | ||
} | ||
|
||
} | ||
|
||
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) { | ||
// route5 | ||
if ($pathinfo === '/route5') { | ||
if ('/route5' === $pathinfo) { | ||
return array('_route' => 'route5'); | ||
} | ||
|
||
} | ||
|
||
// route6 | ||
if ($pathinfo === '/route6') { | ||
if ('/route6' === $pathinfo) { | ||
return array('_route' => 'route6'); | ||
} | ||
|
||
if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#si', $host, $hostMatches)) { | ||
if (0 === strpos($pathinfo, '/route1')) { | ||
// route11 | ||
if ($pathinfo === '/route11') { | ||
if ('/route11' === $pathinfo) { | ||
return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route11')), array ()); | ||
} | ||
|
||
// route12 | ||
if ($pathinfo === '/route12') { | ||
if ('/route12' === $pathinfo) { | ||
return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route12')), array ( 'var1' => 'val',)); | ||
} | ||
|
||
|
@@ -280,15 +288,15 @@ public function match($pathinfo) | |
} | ||
|
||
// route17 | ||
if ($pathinfo === '/route17') { | ||
if ('/route17' === $pathinfo) { | ||
return array('_route' => 'route17'); | ||
} | ||
|
||
} | ||
|
||
if (0 === strpos($pathinfo, '/a')) { | ||
// a | ||
if ($pathinfo === '/a/a...') { | ||
if ('/a/a...' === $pathinfo) { | ||
return array('_route' => 'a'); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be
$scheme
here, not$schema
, right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fabpot correct, I've fixed it.