18
18
* PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes.
19
19
*
20
20
* @author Fabien Potencier <fabien@symfony.com>
21
+ * @author Tobias Schultze <http://tobion.de>
21
22
*/
22
23
class PhpMatcherDumper extends MatcherDumper
23
24
{
@@ -42,23 +43,49 @@ public function dump(array $options = array())
42
43
43
44
// trailing slash support is only enabled if we know how to redirect the user
44
45
$ interfaces = class_implements ($ options ['base_class ' ]);
45
- $ supportsRedirections = isset ($ interfaces ['Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface ' ]);
46
-
47
- return
48
- $ this ->startClass ($ options ['class ' ], $ options ['base_class ' ]).
49
- $ this ->addConstructor ().
50
- $ this ->addMatcher ($ supportsRedirections ).
51
- $ this ->endClass ()
52
- ;
46
+ $ supportsRedirections = isset ($ interfaces ['Symfony \\Component \\Routing \\Matcher \\RedirectableUrlMatcherInterface ' ]);
47
+
48
+ return <<<EOF
49
+ <?php
50
+
51
+ use Symfony\Component\Routing\Exception\MethodNotAllowedException;
52
+ use Symfony\Component\Routing\Exception\ResourceNotFoundException;
53
+ use Symfony\Component\Routing\RequestContext;
54
+
55
+ /**
56
+ * {$ options ['class ' ]}
57
+ *
58
+ * This class has been auto-generated
59
+ * by the Symfony Routing Component.
60
+ */
61
+ class {$ options ['class ' ]} extends {$ options ['base_class ' ]}
62
+ {
63
+ /**
64
+ * Constructor.
65
+ */
66
+ public function __construct(RequestContext \$context)
67
+ {
68
+ \$this->context = \$context;
53
69
}
54
70
55
- private function addMatcher ($ supportsRedirections )
71
+ {$ this ->generateMatchMethod ($ supportsRedirections )}
72
+ }
73
+
74
+ EOF ;
75
+ }
76
+
77
+ /**
78
+ * Generates the code for the match method implementing UrlMatcherInterface.
79
+ *
80
+ * @param Boolean $supportsRedirections Whether redirections are supported by the base class
81
+ *
82
+ * @return string Match method as PHP code
83
+ */
84
+ private function generateMatchMethod ($ supportsRedirections )
56
85
{
57
- // we need to deep clone the routes as we will modify the structure to optimize the dump
58
- $ code = rtrim ($ this ->compileRoutes (clone $ this ->getRoutes (), $ supportsRedirections ), "\n" );
86
+ $ code = rtrim ($ this ->compileRoutes ($ this ->getRoutes (), $ supportsRedirections ), "\n" );
59
87
60
88
return <<<EOF
61
-
62
89
public function match( \$pathinfo)
63
90
{
64
91
\$allow = array();
@@ -68,78 +95,83 @@ public function match(\$pathinfo)
68
95
69
96
throw 0 < count( \$allow) ? new MethodNotAllowedException(array_unique( \$allow)) : new ResourceNotFoundException();
70
97
}
71
-
72
98
EOF ;
73
99
}
74
100
101
+ /**
102
+ * Counts the number of routes as direct child of the RouteCollection.
103
+ *
104
+ * @param RouteCollection $routes A RouteCollection instance
105
+ *
106
+ * @return integer Number of Routes
107
+ */
108
+ private function countDirectChildRoutes (RouteCollection $ routes )
109
+ {
110
+ $ count = 0 ;
111
+ foreach ($ routes as $ route ) {
112
+ if ($ route instanceof Route) {
113
+ $ count ++;
114
+ }
115
+ }
116
+
117
+ return $ count ;
118
+ }
119
+
120
+ /**
121
+ * Generates PHP code recursively to match a RouteCollection with all child routes and child collections.
122
+ *
123
+ * @param RouteCollection $routes A RouteCollection instance
124
+ * @param Boolean $supportsRedirections Whether redirections are supported by the base class
125
+ * @param string|null $parentPrefix The prefix of the parent collection used to optimize the code
126
+ *
127
+ * @return string PHP code
128
+ */
75
129
private function compileRoutes (RouteCollection $ routes , $ supportsRedirections , $ parentPrefix = null )
76
130
{
77
131
$ code = '' ;
78
132
79
- $ routeIterator = $ routes ->getIterator ();
80
- $ keys = array_keys ($ routeIterator ->getArrayCopy ());
81
- $ keysCount = count ($ keys );
82
-
83
- $ i = 0 ;
84
- foreach ($ routeIterator as $ name => $ route ) {
85
- $ i ++;
86
-
87
- if ($ route instanceof RouteCollection) {
88
- $ prefix = $ route ->getPrefix ();
89
- $ optimizable = $ prefix && count ($ route ->all ()) > 1 && false === strpos ($ route ->getPrefix (), '{ ' );
90
- $ optimized = false ;
91
-
92
- if ($ optimizable ) {
93
- for ($ j = $ i ; $ j < $ keysCount ; $ j ++) {
94
- if (null === $ keys [$ j ]) {
95
- continue ;
96
- }
97
-
98
- $ testRoute = $ routeIterator ->offsetGet ($ keys [$ j ]);
99
- $ isCollection = ($ testRoute instanceof RouteCollection);
100
-
101
- $ testPrefix = $ isCollection ? $ testRoute ->getPrefix () : $ testRoute ->getPattern ();
102
-
103
- if (0 === strpos ($ testPrefix , $ prefix )) {
104
- $ routeIterator ->offsetUnset ($ keys [$ j ]);
105
-
106
- if ($ isCollection ) {
107
- $ route ->addCollection ($ testRoute );
108
- } else {
109
- $ route ->add ($ keys [$ j ], $ testRoute );
110
- }
111
-
112
- $ i ++;
113
- $ keys [$ j ] = null ;
114
- }
115
- }
116
-
117
- if ($ prefix !== $ parentPrefix ) {
118
- $ code .= sprintf (" if (0 === strpos( \$pathinfo, %s)) { \n" , var_export ($ prefix , true ));
119
- $ optimized = true ;
120
- }
121
- }
122
-
123
- if ($ optimized ) {
124
- foreach (explode ("\n" , $ this ->compileRoutes ($ route , $ supportsRedirections , $ prefix )) as $ line ) {
125
- if ('' !== $ line ) {
126
- $ code .= ' ' ; // apply extra indention
127
- }
128
- $ code .= $ line ."\n" ;
129
- }
130
- $ code = substr ($ code , 0 , -2 ); // remove redundant last two line breaks
131
- $ code .= " } \n\n" ;
132
- } else {
133
- $ code .= $ this ->compileRoutes ($ route , $ supportsRedirections , $ prefix );
134
- }
135
- } else {
136
- $ code .= $ this ->compileRoute ($ route , $ name , $ supportsRedirections , $ parentPrefix )."\n" ;
133
+ $ prefix = $ routes ->getPrefix ();
134
+ $ countDirectChildRoutes = $ this ->countDirectChildRoutes ($ routes );
135
+ $ countAllChildRoutes = count ($ routes ->all ());
136
+ // Can the matching be optimized by wrapping it with the prefix condition
137
+ // - no need to optimize if current prefix is the same as the parent prefix
138
+ // - if $countDirectChildRoutes === 0, the sub-collections can do their own optimizations (in case there are any)
139
+ // - it's not worth wrapping a single child route
140
+ // - prefixes with variables cannot be optimized because routes within the collection might have different requirements for the same variable
141
+ $ optimizable = '' !== $ prefix && $ prefix !== $ parentPrefix && $ countDirectChildRoutes > 0 && $ countAllChildRoutes > 1 && false === strpos ($ prefix , '{ ' );
142
+ if ($ optimizable ) {
143
+ $ code .= sprintf (" if (0 === strpos( \$pathinfo, %s)) { \n" , var_export ($ prefix , true ));
144
+ }
145
+
146
+ foreach ($ routes as $ name => $ route ) {
147
+ if ($ route instanceof Route) {
148
+ // a single route in a sub-collection is not wrapped so it should do its own optimization in ->compileRoute with $parentPrefix = null
149
+ $ code .= $ this ->compileRoute ($ route , $ name , $ supportsRedirections , 1 === $ countAllChildRoutes ? null : $ prefix )."\n" ;
150
+ } elseif ($ countAllChildRoutes - $ countDirectChildRoutes > 0 ) { // we can stop iterating recursively if we already know there are no more routes
151
+ $ code .= $ this ->compileRoutes ($ route , $ supportsRedirections , $ prefix );
137
152
}
138
153
}
139
154
155
+ if ($ optimizable ) {
156
+ $ code .= " } \n\n" ;
157
+ // apply extra indention at each line (except empty ones)
158
+ $ code = preg_replace ('/^.{2,}$/m ' , ' $0 ' , $ code );
159
+ }
160
+
140
161
return $ code ;
141
162
}
142
163
164
+
165
+ /**
166
+ * Compiles a single Route to PHP code used to match it against the path info.
167
+ *
168
+ * @param Route $routes A Route instance
169
+ * @param string $name The name of the Route
170
+ * @param Boolean $supportsRedirections Whether redirections are supported by the base class
171
+ * @param string|null $parentPrefix The prefix of the parent collection used to optimize the code
172
+ *
173
+ * @return string PHP code
174
+ */
143
175
private function compileRoute (Route $ route , $ name , $ supportsRedirections , $ parentPrefix = null )
144
176
{
145
177
$ code = '' ;
@@ -167,7 +199,7 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
167
199
$ conditions [] = sprintf ("\$pathinfo === %s " , var_export (str_replace ('\\' , '' , $ m ['url ' ]), true ));
168
200
}
169
201
} else {
170
- if ($ compiledRoute ->getStaticPrefix () && $ compiledRoute ->getStaticPrefix () != $ parentPrefix ) {
202
+ if ($ compiledRoute ->getStaticPrefix () && $ compiledRoute ->getStaticPrefix () !== $ parentPrefix ) {
171
203
$ conditions [] = sprintf ("0 === strpos( \$pathinfo, %s) " , var_export ($ compiledRoute ->getStaticPrefix (), true ));
172
204
}
173
205
@@ -254,47 +286,4 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
254
286
255
287
return $ code ;
256
288
}
257
-
258
- private function startClass ($ class , $ baseClass )
259
- {
260
- return <<<EOF
261
- <?php
262
-
263
- use Symfony\Component\Routing\Exception\MethodNotAllowedException;
264
- use Symfony\Component\Routing\Exception\ResourceNotFoundException;
265
- use Symfony\Component\Routing\RequestContext;
266
-
267
- /**
268
- * $ class
269
- *
270
- * This class has been auto-generated
271
- * by the Symfony Routing Component.
272
- */
273
- class $ class extends $ baseClass
274
- {
275
-
276
- EOF ;
277
- }
278
-
279
- private function addConstructor ()
280
- {
281
- return <<<EOF
282
- /**
283
- * Constructor.
284
- */
285
- public function __construct(RequestContext \$context)
286
- {
287
- \$this->context = \$context;
288
- }
289
-
290
- EOF ;
291
- }
292
-
293
- private function endClass ()
294
- {
295
- return <<<EOF
296
- }
297
-
298
- EOF ;
299
- }
300
289
}
0 commit comments