Skip to content

Commit cb4e856

Browse files
committed
Better handling of exact route match
no need to use regex matching if route path and url path are identical
1 parent cf5da01 commit cb4e856

File tree

3 files changed

+146
-57
lines changed

3 files changed

+146
-57
lines changed

src/Cortex/Router/Router.php

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,17 @@ public function match(UriInterface $uri, $httpMethod)
8787
return $this->results;
8888
}
8989

90-
if (! count($this->routes) || ! $this->parseRoutes($uri, $httpMethod)) {
90+
if (! count($this->routes) || !$this->parseRoutes($uri, $httpMethod)) {
9191
$this->results = new MatchingResult(['route' => null]);
9292

9393
return $this->results;
9494
}
9595

96+
// in case of exact match, no need to go further
97+
if ($this->results instanceof MatchingResult) {
98+
return $this->results;
99+
}
100+
96101
$dispatcher = $this->buildDispatcher($this->collector->getData());
97102
unset($this->collector);
98103

@@ -121,21 +126,25 @@ private function parseRoutes(UriInterface $uri, $httpMethod)
121126
{
122127
$iterator = new RouteFilterIterator($this->routes, $uri);
123128
$parsed = 0;
124-
$iterator->rewind();
125-
while ($iterator->valid()) {
126-
/** @var \Brain\Cortex\Route\RouteInterface $route */
127-
$route = $this->groups->mergeGroup($iterator->current());
128-
if (empty($route['method']) || strtolower($route['method']) === 'any') {
129-
$route['method'] = $httpMethod;
129+
/** @var \Brain\Cortex\Route\RouteInterface $route */
130+
foreach ($iterator as $route) {
131+
$route = $this->sanitizeRouteMethod($this->groups->mergeGroup($route), $httpMethod);
132+
if (!$this->validateRoute($route, $uri, $httpMethod)) {
133+
continue;
130134
}
131-
if ($route instanceof RouteInterface && $this->validate($route)) {
132-
$id = $route->id();
133-
$this->parsedRoutes[$id] = $route;
134-
$path = '/'.trim($route['path'], '/');
135-
$this->collector->addRoute(strtoupper($route['method']), $path, $id);
136-
$parsed++;
135+
136+
$parsed++;
137+
$id = $route->id();
138+
$this->parsedRoutes[$id] = $route;
139+
$path = '/'.trim($route['path'], '/');
140+
// exact match
141+
if ($path === '/'.trim($uri->path(), '/')) {
142+
$this->results = $this->finalizeRoute($route, [], $uri);
143+
unset($this->parsedRoutes, $this->collector);
144+
break;
137145
}
138-
$iterator->next();
146+
147+
$this->collector->addRoute(strtoupper($route['method']), $path, $id);
139148
}
140149

141150
unset($this->routes, $this->groups);
@@ -145,30 +154,49 @@ private function parseRoutes(UriInterface $uri, $httpMethod)
145154

146155
/**
147156
* @param \Brain\Cortex\Route\RouteInterface $route
157+
* @param string $httpMethod
158+
* @return \Brain\Cortex\Route\RouteInterface
159+
*/
160+
private function sanitizeRouteMethod(RouteInterface $route, $httpMethod)
161+
{
162+
if (empty($route['method']) || !(is_string($route['method']) || is_array($route['method']))) {
163+
$route['method'] = $httpMethod;
164+
}
165+
166+
if (is_array($route['method'])) {
167+
$route['method'] = array_map('strtoupper', array_filter($route['method'], 'is_string'));
168+
169+
return $route;
170+
}
171+
172+
(strtolower($route['method']) === 'any') and $route['method'] = $httpMethod;
173+
174+
$route['method'] = strtoupper($route['method']);
175+
176+
return $route;
177+
}
178+
179+
/**
180+
* @param \Brain\Cortex\Route\RouteInterface $route
181+
* @param \Brain\Cortex\Uri\UriInterface $uri
182+
* @param $httpMethod
148183
* @return bool
149184
*/
150-
private function validate(RouteInterface $route)
185+
private function validateRoute(RouteInterface $route, UriInterface $uri, $httpMethod)
151186
{
152187
$id = $route->id();
153-
$path = $route['path'];
154-
$method = $route['method'];
188+
$path = trim($route['path'], '/');
189+
if (count($uri->chunks()) !== (substr_count($path, '/') + 1)) {
190+
return false;
191+
}
192+
$method = (array) $route['method'];
155193
$handler = $route['handler'];
156-
$methods = [
157-
'GET',
158-
'POST',
159-
'PUT',
160-
'OPTIONS',
161-
'HEAD',
162-
'DELETE',
163-
'TRACE',
164-
'CONNECT',
165-
];
166194

167195
return
168196
is_string($id)
169197
&& $id
170198
&& filter_var($path, FILTER_SANITIZE_URL) === $path
171-
&& in_array(strtoupper((string) $method), $methods, true)
199+
&& in_array($httpMethod, $method, true)
172200
&& (is_callable($handler) || $handler instanceof ControllerInterface);
173201
}
174202

@@ -203,7 +231,7 @@ private function finalizeRoute(RouteInterface $route, array $vars, UriInterface
203231
$result = null;
204232
if (is_callable($route['vars'])) {
205233
$cb = $route['vars'];
206-
$routeVars = $cb($vars);
234+
$routeVars = $cb($vars, $uri);
207235
is_array($routeVars) and $vars = $routeVars;
208236
$routeVars instanceof MatchingResult and $result = $routeVars;
209237
} elseif (is_array($route['vars'])) {

tests/src/Functional/CortexTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ public function testCortexNotMatchBecauseUrlChangedViaFilter()
336336
$uri->shouldReceive('host')->andReturn('example.com');
337337
$uri->shouldReceive('vars')->andReturn([]);
338338
$uri->shouldReceive('path')->andReturn('meh/meh/meh'); // this does not match
339+
$uri->shouldReceive('chunks')->andReturn(['meh', 'meh', 'meh']);
339340

340341
return $uri;
341342
});

tests/src/Unit/Router/RouterTest.php

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public function testMatchNothingIfNoValidatingRoutes()
118118
$uri = \Mockery::mock(UriInterface::class);
119119
$uri->shouldReceive('scheme')->andReturn('http');
120120
$uri->shouldReceive('host')->andReturn('example.com');
121+
$uri->shouldReceive('chunks')->andReturn([]);
121122

122123
$router = new Router($routes, $groups);
123124
$result = $router->match($uri, 'GET');
@@ -161,6 +162,7 @@ public function testMatchNotMatching()
161162
$uri->shouldReceive('scheme')->andReturn('http');
162163
$uri->shouldReceive('host')->andReturn('example.com');
163164
$uri->shouldReceive('path')->andReturn('bar');
165+
$uri->shouldReceive('chunks')->andReturn(['bar']);
164166

165167
$collector = \Mockery::mock(RouteCollector::class);
166168
$collector->shouldReceive('addRoute')->once()->with('POST', '/foo', 'r1')->andReturnNull();
@@ -196,7 +198,7 @@ public function testMatchNotMatching()
196198
assertSame($expected, $data);
197199
}
198200

199-
public function testMatchMatching()
201+
public function testMatchMatchingExactMatch()
200202
{
201203
$handler = function () {
202204
return func_get_args();
@@ -221,17 +223,13 @@ public function testMatchMatching()
221223
$uri->shouldReceive('host')->andReturn('example.com');
222224
$uri->shouldReceive('path')->andReturn('foo');
223225
$uri->shouldReceive('vars')->once()->andReturn(['c' => 'C']);
226+
$uri->shouldReceive('chunks')->andReturn(['foo']);
224227

225228
$collector = \Mockery::mock(RouteCollector::class);
226-
$collector->shouldReceive('addRoute')->once()->with('POST', '/foo', 'r1')->andReturnNull();
227-
$collector->shouldReceive('getData')->once()->andReturn(['foo' => 'bar']);
229+
$collector->shouldReceive('addRoute')->never();
228230

229231
$dispatcher = \Mockery::mock(Dispatcher::class);
230-
$dispatcher->shouldReceive('dispatch')->with('POST', '/foo')->andReturn([
231-
Dispatcher::FOUND,
232-
'r1',
233-
['a' => 'A', 'b' => 'B'],
234-
]);
232+
$dispatcher->shouldReceive('dispatch')->never();
235233

236234
$factory = function (array $args) use ($dispatcher) {
237235
assertSame($args, ['foo' => 'bar']);
@@ -245,7 +243,7 @@ public function testMatchMatching()
245243
$expected = [
246244
'route' => 'r1',
247245
'path' => '/foo',
248-
'vars' => ['a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'],
246+
'vars' => ['c' => 'C', 'd' => 'D'],
249247
'handler' => $handler,
250248
'before' => null,
251249
'after' => null,
@@ -260,7 +258,75 @@ public function testMatchMatching()
260258
assertSame($expected, $data);
261259
}
262260

263-
public function testMatchMatchingNoQueryVars()
261+
public function testMatchDynamicMatch()
262+
{
263+
$handler = function () {
264+
return func_get_args();
265+
};
266+
267+
$route = new Route([
268+
'id' => 'r1',
269+
'path' => '/foo/{bar}',
270+
'handler' => $handler,
271+
'vars' => ['d' => 'D'],
272+
'method' => 'POST',
273+
]);
274+
$routes = new PriorityRouteCollection();
275+
276+
$routes->addRoute($route);
277+
278+
$groups = \Mockery::mock(GroupCollectionInterface::class);
279+
$groups->shouldReceive('mergeGroup')->once()->with($route)->andReturn($route);
280+
281+
$uri = \Mockery::mock(UriInterface::class);
282+
$uri->shouldReceive('scheme')->andReturn('http');
283+
$uri->shouldReceive('host')->andReturn('example.com');
284+
$uri->shouldReceive('path')->andReturn('foo/i-am-bar');
285+
$uri->shouldReceive('vars')->once()->andReturn(['c' => 'C']);
286+
$uri->shouldReceive('chunks')->andReturn(['foo', 'meh']);
287+
288+
$collector = \Mockery::mock(RouteCollector::class);
289+
$collector->shouldReceive('addRoute')->once()->with('POST', '/foo/{bar}', 'r1');
290+
$collector->shouldReceive('getData')->once()->andReturn(['foo' => 'bar']);
291+
292+
$dispatcher = \Mockery::mock(Dispatcher::class);
293+
$dispatcher->shouldReceive('dispatch')
294+
->once()
295+
->with('POST', '/foo/i-am-bar')
296+
->andReturn([
297+
Dispatcher::FOUND,
298+
'r1',
299+
['bar' => 'i-am-bar'],
300+
]);
301+
302+
$factory = function (array $args) use ($dispatcher) {
303+
assertSame($args, ['foo' => 'bar']);
304+
305+
return $dispatcher;
306+
};
307+
308+
$router = new Router($routes, $groups, $collector, $factory);
309+
$result = $router->match($uri, 'POST');
310+
311+
$expected = [
312+
'route' => 'r1',
313+
'path' => '/foo/{bar}',
314+
'vars' => ['bar' => 'i-am-bar', 'c' => 'C', 'd' => 'D'],
315+
'handler' => $handler,
316+
'before' => null,
317+
'after' => null,
318+
'template' => null,
319+
];
320+
321+
$proxy = new Proxy($result);
322+
/** @noinspection PhpUndefinedFieldInspection */
323+
$data = $proxy->data;
324+
325+
assertTrue($result->matched());
326+
assertSame($expected, $data);
327+
}
328+
329+
public function testMatchMatchingExactMatchNoQueryVars()
264330
{
265331
$handler = function () {
266332
return func_get_args();
@@ -285,17 +351,13 @@ public function testMatchMatchingNoQueryVars()
285351
$uri->shouldReceive('scheme')->andReturn('http');
286352
$uri->shouldReceive('host')->andReturn('example.com');
287353
$uri->shouldReceive('path')->andReturn('foo');
354+
$uri->shouldReceive('chunks')->andReturn(['foo']);
288355

289356
$collector = \Mockery::mock(RouteCollector::class);
290-
$collector->shouldReceive('addRoute')->once()->with('POST', '/foo', 'r1')->andReturnNull();
291-
$collector->shouldReceive('getData')->once()->andReturn(['foo' => 'bar']);
357+
$collector->shouldReceive('addRoute')->never();
292358

293359
$dispatcher = \Mockery::mock(Dispatcher::class);
294-
$dispatcher->shouldReceive('dispatch')->with('POST', '/foo')->andReturn([
295-
Dispatcher::FOUND,
296-
'r1',
297-
['a' => 'A', 'b' => 'B'],
298-
]);
360+
$dispatcher->shouldReceive('dispatch')->never();
299361

300362
$factory = function (array $args) use ($dispatcher) {
301363
assertSame($args, ['foo' => 'bar']);
@@ -309,7 +371,7 @@ public function testMatchMatchingNoQueryVars()
309371
$expected = [
310372
'route' => 'r1',
311373
'path' => '/foo',
312-
'vars' => ['a' => 'A', 'b' => 'B', 'd' => 'D'],
374+
'vars' => ['d' => 'D'],
313375
'handler' => $handler,
314376
'before' => null,
315377
'after' => null,
@@ -324,7 +386,7 @@ public function testMatchMatchingNoQueryVars()
324386
assertSame($expected, $data);
325387
}
326388

327-
public function testMatchMatchingCallableVars()
389+
public function testMatchMatchingExactMatchCallableVars()
328390
{
329391
$handler = function () {
330392
return func_get_args();
@@ -334,7 +396,9 @@ public function testMatchMatchingCallableVars()
334396
'id' => 'r1',
335397
'path' => '/foo',
336398
'handler' => $handler,
337-
'vars' => 'array_keys',
399+
'vars' => function (array $vars) {
400+
return array_keys($vars);
401+
},
338402
'method' => 'POST',
339403
]);
340404
$routes = new PriorityRouteCollection();
@@ -349,17 +413,13 @@ public function testMatchMatchingCallableVars()
349413
$uri->shouldReceive('host')->andReturn('example.com');
350414
$uri->shouldReceive('path')->andReturn('foo');
351415
$uri->shouldReceive('vars')->once()->andReturn(['c' => 'C']);
416+
$uri->shouldReceive('chunks')->andReturn(['foo']);
352417

353418
$collector = \Mockery::mock(RouteCollector::class);
354-
$collector->shouldReceive('addRoute')->once()->with('POST', '/foo', 'r1')->andReturnNull();
355-
$collector->shouldReceive('getData')->once()->andReturn(['foo' => 'bar']);
419+
$collector->shouldReceive('addRoute')->never();
356420

357421
$dispatcher = \Mockery::mock(Dispatcher::class);
358-
$dispatcher->shouldReceive('dispatch')->with('POST', '/foo')->andReturn([
359-
Dispatcher::FOUND,
360-
'r1',
361-
['a' => 'A', 'b' => 'B'],
362-
]);
422+
$dispatcher->shouldReceive('dispatch')->never();
363423

364424
$factory = function (array $args) use ($dispatcher) {
365425
assertSame($args, ['foo' => 'bar']);
@@ -373,7 +433,7 @@ public function testMatchMatchingCallableVars()
373433
$expected = [
374434
'route' => 'r1',
375435
'path' => '/foo',
376-
'vars' => ['a', 'b', 'c'],
436+
'vars' => ['c'],
377437
'handler' => $handler,
378438
'before' => null,
379439
'after' => null,

0 commit comments

Comments
 (0)