PhpMatcherDumper.php 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Routing\Matcher\Dumper;
  11. use Symfony\Component\Routing\Route;
  12. use Symfony\Component\Routing\RouteCollection;
  13. /**
  14. * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes.
  15. *
  16. * @author Fabien Potencier <fabien@symfony.com>
  17. */
  18. class PhpMatcherDumper extends MatcherDumper
  19. {
  20. /**
  21. * Dumps a set of routes to a PHP class.
  22. *
  23. * Available options:
  24. *
  25. * * class: The class name
  26. * * base_class: The base class name
  27. *
  28. * @param array $options An array of options
  29. *
  30. * @return string A PHP class representing the matcher class
  31. */
  32. public function dump(array $options = array())
  33. {
  34. $options = array_merge(array(
  35. 'class' => 'ProjectUrlMatcher',
  36. 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
  37. ), $options);
  38. // trailing slash support is only enabled if we know how to redirect the user
  39. $interfaces = class_implements($options['base_class']);
  40. $supportsRedirections = isset($interfaces['Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface']);
  41. return
  42. $this->startClass($options['class'], $options['base_class']).
  43. $this->addConstructor().
  44. $this->addMatcher($supportsRedirections).
  45. $this->endClass()
  46. ;
  47. }
  48. private function addMatcher($supportsRedirections)
  49. {
  50. // we need to deep clone the routes as we will modify the structure to optimize the dump
  51. $code = implode("\n", $this->compileRoutes(clone $this->getRoutes(), $supportsRedirections));
  52. return <<<EOF
  53. public function match(\$pathinfo)
  54. {
  55. \$allow = array();
  56. \$pathinfo = urldecode(\$pathinfo);
  57. $code
  58. throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
  59. }
  60. EOF;
  61. }
  62. private function compileRoutes(RouteCollection $routes, $supportsRedirections, $parentPrefix = null)
  63. {
  64. $code = array();
  65. $routeIterator = $routes->getIterator();
  66. $keys = array_keys($routeIterator->getArrayCopy());
  67. $keysCount = count($keys);
  68. $i = 0;
  69. foreach ($routeIterator as $name => $route) {
  70. $i++;
  71. if ($route instanceof RouteCollection) {
  72. $prefix = $route->getPrefix();
  73. $optimizable = $prefix && count($route->all()) > 1 && false === strpos($route->getPrefix(), '{');
  74. $indent = '';
  75. if ($optimizable) {
  76. for ($j = $i; $j < $keysCount; $j++) {
  77. if ($keys[$j] === null) {
  78. continue;
  79. }
  80. $testRoute = $routeIterator->offsetGet($keys[$j]);
  81. $isCollection = ($testRoute instanceof RouteCollection);
  82. $testPrefix = $isCollection ? $testRoute->getPrefix() : $testRoute->getPattern();
  83. if (0 === strpos($testPrefix, $prefix)) {
  84. $routeIterator->offsetUnset($keys[$j]);
  85. if ($isCollection) {
  86. $route->addCollection($testRoute);
  87. } else {
  88. $route->add($keys[$j], $testRoute);
  89. }
  90. $i++;
  91. $keys[$j] = null;
  92. }
  93. }
  94. if ($prefix !== $parentPrefix) {
  95. $code[] = sprintf(" if (0 === strpos(\$pathinfo, %s)) {", var_export($prefix, true));
  96. $indent = ' ';
  97. }
  98. }
  99. foreach ($this->compileRoutes($route, $supportsRedirections, $prefix) as $line) {
  100. foreach (explode("\n", $line) as $l) {
  101. if ($l) {
  102. $code[] = $indent.$l;
  103. } else {
  104. $code[] = $l;
  105. }
  106. }
  107. }
  108. if ($optimizable && $prefix !== $parentPrefix) {
  109. $code[] = " }\n";
  110. }
  111. } else {
  112. foreach ($this->compileRoute($route, $name, $supportsRedirections, $parentPrefix) as $line) {
  113. $code[] = $line;
  114. }
  115. }
  116. }
  117. return $code;
  118. }
  119. private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null)
  120. {
  121. $code = array();
  122. $compiledRoute = $route->compile();
  123. $conditions = array();
  124. $hasTrailingSlash = false;
  125. $matches = false;
  126. if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
  127. if ($supportsRedirections && substr($m['url'], -1) === '/') {
  128. $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
  129. $hasTrailingSlash = true;
  130. } else {
  131. $conditions[] = sprintf("\$pathinfo === %s", var_export(str_replace('\\', '', $m['url']), true));
  132. }
  133. } else {
  134. if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() != $parentPrefix) {
  135. $conditions[] = sprintf("0 === strpos(\$pathinfo, %s)", var_export($compiledRoute->getStaticPrefix(), true));
  136. }
  137. $regex = $compiledRoute->getRegex();
  138. if ($supportsRedirections && $pos = strpos($regex, '/$')) {
  139. $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
  140. $hasTrailingSlash = true;
  141. }
  142. $conditions[] = sprintf("preg_match(%s, \$pathinfo, \$matches)", var_export($regex, true));
  143. $matches = true;
  144. }
  145. $conditions = implode(' && ', $conditions);
  146. $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
  147. $code[] = <<<EOF
  148. // $name
  149. if ($conditions) {
  150. EOF;
  151. if ($req = $route->getRequirement('_method')) {
  152. $methods = explode('|', strtoupper($req));
  153. // GET and HEAD are equivalent
  154. if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
  155. $methods[] = 'HEAD';
  156. }
  157. if (1 === count($methods)) {
  158. $code[] = <<<EOF
  159. if (\$this->context->getMethod() != '$methods[0]') {
  160. \$allow[] = '$methods[0]';
  161. goto $gotoname;
  162. }
  163. EOF;
  164. } else {
  165. $methods = implode('\', \'', $methods);
  166. $code[] = <<<EOF
  167. if (!in_array(\$this->context->getMethod(), array('$methods'))) {
  168. \$allow = array_merge(\$allow, array('$methods'));
  169. goto $gotoname;
  170. }
  171. EOF;
  172. }
  173. }
  174. if ($hasTrailingSlash) {
  175. $code[] = sprintf(<<<EOF
  176. if (substr(\$pathinfo, -1) !== '/') {
  177. return \$this->redirect(\$pathinfo.'/', '%s');
  178. }
  179. EOF
  180. , $name);
  181. }
  182. if ($scheme = $route->getRequirement('_scheme')) {
  183. if (!$supportsRedirections) {
  184. throw new \LogicException('The "_scheme" requirement is only supported for route dumper that implements RedirectableUrlMatcherInterface.');
  185. }
  186. $code[] = sprintf(<<<EOF
  187. if (\$this->context->getScheme() !== '$scheme') {
  188. return \$this->redirect(\$pathinfo, '%s', '$scheme');
  189. }
  190. EOF
  191. , $name);
  192. }
  193. // optimize parameters array
  194. if (true === $matches && $compiledRoute->getDefaults()) {
  195. $code[] = sprintf(" return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));"
  196. , str_replace("\n", '', var_export($compiledRoute->getDefaults(), true)), $name);
  197. } elseif (true === $matches) {
  198. $code[] = sprintf(" \$matches['_route'] = '%s';", $name);
  199. $code[] = sprintf(" return \$matches;", $name);
  200. } elseif ($compiledRoute->getDefaults()) {
  201. $code[] = sprintf(' return %s;', str_replace("\n", '', var_export(array_merge($compiledRoute->getDefaults(), array('_route' => $name)), true)));
  202. } else {
  203. $code[] = sprintf(" return array('_route' => '%s');", $name);
  204. }
  205. $code[] = " }";
  206. if ($req) {
  207. $code[] = " $gotoname:";
  208. }
  209. $code[] = '';
  210. return $code;
  211. }
  212. private function startClass($class, $baseClass)
  213. {
  214. return <<<EOF
  215. <?php
  216. use Symfony\Component\Routing\Exception\MethodNotAllowedException;
  217. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  218. use Symfony\Component\Routing\RequestContext;
  219. /**
  220. * $class
  221. *
  222. * This class has been auto-generated
  223. * by the Symfony Routing Component.
  224. */
  225. class $class extends $baseClass
  226. {
  227. EOF;
  228. }
  229. private function addConstructor()
  230. {
  231. return <<<EOF
  232. /**
  233. * Constructor.
  234. */
  235. public function __construct(RequestContext \$context)
  236. {
  237. \$this->context = \$context;
  238. }
  239. EOF;
  240. }
  241. private function endClass()
  242. {
  243. return <<<EOF
  244. }
  245. EOF;
  246. }
  247. }