CompiledRouteCollectionTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <?php
  2. namespace Illuminate\Tests\Integration\Routing;
  3. use ArrayIterator;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Routing\Route;
  6. use Illuminate\Routing\RouteCollection;
  7. use Illuminate\Support\Arr;
  8. use Orchestra\Testbench\TestCase;
  9. use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
  10. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  11. class CompiledRouteCollectionTest extends TestCase
  12. {
  13. /**
  14. * @var \Illuminate\Routing\RouteCollection
  15. */
  16. protected $routeCollection;
  17. /**
  18. * @var \Illuminate\Routing\Router
  19. */
  20. protected $router;
  21. protected function setUp(): void
  22. {
  23. parent::setUp();
  24. $this->router = $this->app['router'];
  25. $this->routeCollection = new RouteCollection;
  26. }
  27. protected function tearDown(): void
  28. {
  29. parent::tearDown();
  30. unset($this->routeCollection, $this->router);
  31. }
  32. /**
  33. * @return \Illuminate\Routing\CompiledRouteCollection
  34. */
  35. protected function collection()
  36. {
  37. return $this->routeCollection->toCompiledRouteCollection($this->router, $this->app);
  38. }
  39. public function testRouteCollectionCanAddRoute()
  40. {
  41. $this->routeCollection->add($this->newRoute('GET', 'foo', [
  42. 'uses' => 'FooController@index',
  43. 'as' => 'foo_index',
  44. ]));
  45. $this->assertCount(1, $this->collection());
  46. }
  47. public function testRouteCollectionAddReturnsTheRoute()
  48. {
  49. $outputRoute = $this->collection()->add($inputRoute = $this->newRoute('GET', 'foo', [
  50. 'uses' => 'FooController@index',
  51. 'as' => 'foo_index',
  52. ]));
  53. $this->assertInstanceOf(Route::class, $outputRoute);
  54. $this->assertEquals($inputRoute, $outputRoute);
  55. }
  56. public function testRouteCollectionCanRetrieveByName()
  57. {
  58. $this->routeCollection->add($routeIndex = $this->newRoute('GET', 'foo/index', [
  59. 'uses' => 'FooController@index',
  60. 'as' => 'route_name',
  61. ]));
  62. $routes = $this->collection();
  63. $this->assertSame('route_name', $routeIndex->getName());
  64. $this->assertSame('route_name', $routes->getByName('route_name')->getName());
  65. $this->assertEquals($routeIndex, $routes->getByName('route_name'));
  66. }
  67. public function testRouteCollectionCanRetrieveByAction()
  68. {
  69. $this->routeCollection->add($routeIndex = $this->newRoute('GET', 'foo/index', $action = [
  70. 'uses' => 'FooController@index',
  71. ]));
  72. $route = $this->collection()->getByAction('FooController@index');
  73. $this->assertSame($action, Arr::except($routeIndex->getAction(), 'as'));
  74. $this->assertSame($action, Arr::except($route->getAction(), 'as'));
  75. }
  76. public function testRouteCollectionCanGetIterator()
  77. {
  78. $this->routeCollection->add($this->newRoute('GET', 'foo/index', [
  79. 'uses' => 'FooController@index',
  80. 'as' => 'foo_index',
  81. ]));
  82. $this->assertInstanceOf(ArrayIterator::class, $this->collection()->getIterator());
  83. }
  84. public function testRouteCollectionCanGetIteratorWhenEmpty()
  85. {
  86. $routes = $this->collection();
  87. $this->assertCount(0, $routes);
  88. $this->assertInstanceOf(ArrayIterator::class, $routes->getIterator());
  89. }
  90. public function testRouteCollectionCanGetIteratorWhenRoutesAreAdded()
  91. {
  92. $this->routeCollection->add($routeIndex = $this->newRoute('GET', 'foo/index', [
  93. 'uses' => 'FooController@index',
  94. 'as' => 'foo_index',
  95. ]));
  96. $routes = $this->collection();
  97. $this->assertCount(1, $routes);
  98. $this->routeCollection->add($routeShow = $this->newRoute('GET', 'bar/show', [
  99. 'uses' => 'BarController@show',
  100. 'as' => 'bar_show',
  101. ]));
  102. $routes = $this->collection();
  103. $this->assertCount(2, $routes);
  104. $this->assertInstanceOf(ArrayIterator::class, $routes->getIterator());
  105. }
  106. public function testRouteCollectionCanHandleSameRoute()
  107. {
  108. $this->routeCollection->add($routeIndex = $this->newRoute('GET', 'foo/index', [
  109. 'uses' => 'FooController@index',
  110. 'as' => 'foo_index',
  111. ]));
  112. $routes = $this->collection();
  113. $this->assertCount(1, $routes);
  114. // Add exactly the same route
  115. $this->routeCollection->add($routeIndex);
  116. $routes = $this->collection();
  117. $this->assertCount(1, $routes);
  118. // Add a non-existing route
  119. $this->routeCollection->add($this->newRoute('GET', 'bar/show', [
  120. 'uses' => 'BarController@show',
  121. 'as' => 'bar_show',
  122. ]));
  123. $routes = $this->collection();
  124. $this->assertCount(2, $routes);
  125. }
  126. public function testRouteCollectionCanGetAllRoutes()
  127. {
  128. $this->routeCollection->add($routeIndex = $this->newRoute('GET', 'foo/index', [
  129. 'uses' => 'FooController@index',
  130. 'as' => 'foo_index',
  131. ]));
  132. $this->routeCollection->add($routeShow = $this->newRoute('GET', 'foo/show', [
  133. 'uses' => 'FooController@show',
  134. 'as' => 'foo_show',
  135. ]));
  136. $this->routeCollection->add($routeNew = $this->newRoute('POST', 'bar', [
  137. 'uses' => 'BarController@create',
  138. 'as' => 'bar_create',
  139. ]));
  140. $allRoutes = [
  141. $routeIndex,
  142. $routeShow,
  143. $routeNew,
  144. ];
  145. $this->assertEquals($allRoutes, $this->collection()->getRoutes());
  146. }
  147. public function testRouteCollectionCanGetRoutesByName()
  148. {
  149. $routesByName = [
  150. 'foo_index' => $this->newRoute('GET', 'foo/index', [
  151. 'uses' => 'FooController@index',
  152. 'as' => 'foo_index',
  153. ]),
  154. 'foo_show' => $this->newRoute('GET', 'foo/show', [
  155. 'uses' => 'FooController@show',
  156. 'as' => 'foo_show',
  157. ]),
  158. 'bar_create' => $this->newRoute('POST', 'bar', [
  159. 'uses' => 'BarController@create',
  160. 'as' => 'bar_create',
  161. ]),
  162. ];
  163. $this->routeCollection->add($routesByName['foo_index']);
  164. $this->routeCollection->add($routesByName['foo_show']);
  165. $this->routeCollection->add($routesByName['bar_create']);
  166. $this->assertEquals($routesByName, $this->collection()->getRoutesByName());
  167. }
  168. public function testRouteCollectionCanGetRoutesByMethod()
  169. {
  170. $routes = [
  171. 'foo_index' => $this->newRoute('GET', 'foo/index', [
  172. 'uses' => 'FooController@index',
  173. 'as' => 'foo_index',
  174. ]),
  175. 'foo_show' => $this->newRoute('GET', 'foo/show', [
  176. 'uses' => 'FooController@show',
  177. 'as' => 'foo_show',
  178. ]),
  179. 'bar_create' => $this->newRoute('POST', 'bar', [
  180. 'uses' => 'BarController@create',
  181. 'as' => 'bar_create',
  182. ]),
  183. ];
  184. $this->routeCollection->add($routes['foo_index']);
  185. $this->routeCollection->add($routes['foo_show']);
  186. $this->routeCollection->add($routes['bar_create']);
  187. $this->assertEquals([
  188. 'GET' => [
  189. 'foo/index' => $routes['foo_index'],
  190. 'foo/show' => $routes['foo_show'],
  191. ],
  192. 'HEAD' => [
  193. 'foo/index' => $routes['foo_index'],
  194. 'foo/show' => $routes['foo_show'],
  195. ],
  196. 'POST' => [
  197. 'bar' => $routes['bar_create'],
  198. ],
  199. ], $this->collection()->getRoutesByMethod());
  200. }
  201. public function testRouteCollectionCleansUpOverwrittenRoutes()
  202. {
  203. // Create two routes with the same path and method.
  204. $routeA = $this->newRoute('GET', 'product', ['controller' => 'View@view', 'as' => 'routeA']);
  205. $routeB = $this->newRoute('GET', 'product', ['controller' => 'OverwrittenView@view', 'as' => 'overwrittenRouteA']);
  206. $this->routeCollection->add($routeA);
  207. $this->routeCollection->add($routeB);
  208. // Check if the lookups of $routeA and $routeB are there.
  209. $this->assertEquals($routeA, $this->routeCollection->getByName('routeA'));
  210. $this->assertEquals($routeA, $this->routeCollection->getByAction('View@view'));
  211. $this->assertEquals($routeB, $this->routeCollection->getByName('overwrittenRouteA'));
  212. $this->assertEquals($routeB, $this->routeCollection->getByAction('OverwrittenView@view'));
  213. $routes = $this->collection();
  214. // The lookups of $routeA should not be there anymore, because they are no longer valid.
  215. $this->assertNull($routes->getByName('routeA'));
  216. $this->assertNull($routes->getByAction('View@view'));
  217. // The lookups of $routeB are still there.
  218. $this->assertEquals($routeB, $routes->getByName('overwrittenRouteA'));
  219. $this->assertEquals($routeB, $routes->getByAction('OverwrittenView@view'));
  220. }
  221. public function testMatchingThrowsNotFoundExceptionWhenRouteIsNotFound()
  222. {
  223. $this->routeCollection->add($this->newRoute('GET', '/', ['uses' => 'FooController@index']));
  224. $this->expectException(NotFoundHttpException::class);
  225. $this->collection()->match(Request::create('/foo'));
  226. }
  227. public function testMatchingThrowsMethodNotAllowedHttpExceptionWhenMethodIsNotAllowed()
  228. {
  229. $this->routeCollection->add($this->newRoute('GET', '/foo', ['uses' => 'FooController@index']));
  230. $this->expectException(MethodNotAllowedHttpException::class);
  231. $this->collection()->match(Request::create('/foo', 'POST'));
  232. }
  233. public function testMatchingThrowsExceptionWhenMethodIsNotAllowedWhileSameRouteIsAddedDynamically()
  234. {
  235. $this->routeCollection->add($this->newRoute('GET', '/', ['uses' => 'FooController@index']));
  236. $routes = $this->collection();
  237. $routes->add($this->newRoute('POST', '/', ['uses' => 'FooController@index']));
  238. $this->expectException(MethodNotAllowedHttpException::class);
  239. $routes->match(Request::create('/', 'PUT'));
  240. }
  241. public function testMatchingRouteWithSameDynamicallyAddedRouteAlwaysMatchesCachedOneFirst()
  242. {
  243. $this->routeCollection->add(
  244. $route = $this->newRoute('GET', '/', ['uses' => 'FooController@index', 'as' => 'foo'])
  245. );
  246. $routes = $this->collection();
  247. $routes->add($this->newRoute('GET', '/', ['uses' => 'FooController@index', 'as' => 'bar']));
  248. $this->assertSame('foo', $routes->match(Request::create('/', 'GET'))->getName());
  249. }
  250. public function testMatchingFindsRouteWithDifferentMethodDynamically()
  251. {
  252. $this->routeCollection->add($this->newRoute('GET', '/foo', ['uses' => 'FooController@index']));
  253. $routes = $this->collection();
  254. $routes->add($route = $this->newRoute('POST', '/foo', ['uses' => 'FooController@index']));
  255. $this->assertSame($route, $routes->match(Request::create('/foo', 'POST')));
  256. }
  257. public function testMatchingWildcardFromCompiledRoutesAlwaysTakesPrecedent()
  258. {
  259. $this->routeCollection->add(
  260. $route = $this->newRoute('GET', '{wildcard}', ['uses' => 'FooController@index', 'as' => 'foo'])
  261. ->where('wildcard', '.*')
  262. );
  263. $routes = $this->collection();
  264. $routes->add(
  265. $this->newRoute('GET', '{wildcard}', ['uses' => 'FooController@index', 'as' => 'bar'])
  266. ->where('wildcard', '.*')
  267. );
  268. $this->assertSame('foo', $routes->match(Request::create('/foo', 'GET'))->getName());
  269. }
  270. public function testMatchingDynamicallyAddedRoutesTakePrecedenceOverFallbackRoutes()
  271. {
  272. $this->routeCollection->add($this->fallbackRoute(['uses' => 'FooController@index']));
  273. $this->routeCollection->add(
  274. $this->newRoute('GET', '/foo/{id}', ['uses' => 'FooController@index', 'as' => 'foo'])
  275. );
  276. $routes = $this->collection();
  277. $routes->add($this->newRoute('GET', '/bar/{id}', ['uses' => 'FooController@index', 'as' => 'bar']));
  278. $this->assertSame('bar', $routes->match(Request::create('/bar/1', 'GET'))->getName());
  279. }
  280. public function testMatchingFallbackRouteCatchesAll()
  281. {
  282. $this->routeCollection->add($this->fallbackRoute(['uses' => 'FooController@index', 'as' => 'fallback']));
  283. $this->routeCollection->add(
  284. $this->newRoute('GET', '/foo/{id}', ['uses' => 'FooController@index', 'as' => 'foo'])
  285. );
  286. $routes = $this->collection();
  287. $routes->add($this->newRoute('GET', '/bar/{id}', ['uses' => 'FooController@index', 'as' => 'bar']));
  288. $this->assertSame('fallback', $routes->match(Request::create('/baz/1', 'GET'))->getName());
  289. }
  290. public function testMatchingCachedFallbackTakesPrecedenceOverDynamicFallback()
  291. {
  292. $this->routeCollection->add($this->fallbackRoute(['uses' => 'FooController@index', 'as' => 'fallback']));
  293. $routes = $this->collection();
  294. $routes->add($this->fallbackRoute(['uses' => 'FooController@index', 'as' => 'dynamic_fallback']));
  295. $this->assertSame('fallback', $routes->match(Request::create('/baz/1', 'GET'))->getName());
  296. }
  297. public function testMatchingCachedFallbackTakesPrecedenceOverDynamicRouteWithWrongMethod()
  298. {
  299. $this->routeCollection->add($this->fallbackRoute(['uses' => 'FooController@index', 'as' => 'fallback']));
  300. $routes = $this->collection();
  301. $routes->add($this->newRoute('POST', '/bar/{id}', ['uses' => 'FooController@index', 'as' => 'bar']));
  302. $this->assertSame('fallback', $routes->match(Request::create('/bar/1', 'GET'))->getName());
  303. }
  304. public function testSlashPrefixIsProperlyHandled()
  305. {
  306. $this->routeCollection->add($this->newRoute('GET', 'foo/bar', ['uses' => 'FooController@index', 'prefix' => '/']));
  307. $route = $this->collection()->getByAction('FooController@index');
  308. $this->assertSame('foo/bar', $route->uri());
  309. }
  310. public function testRouteWithoutNamespaceIsFound()
  311. {
  312. $this->routeCollection->add($this->newRoute('GET', 'foo/bar', ['controller' => '\App\FooController']));
  313. $route = $this->collection()->getByAction('App\FooController');
  314. $this->assertSame('foo/bar', $route->uri());
  315. }
  316. public function testGroupPrefixAndRoutePrefixAreProperlyHandled()
  317. {
  318. $this->routeCollection->add($this->newRoute('GET', 'foo/bar', ['uses' => 'FooController@index', 'prefix' => '{locale}'])->prefix('pre'));
  319. $route = $this->collection()->getByAction('FooController@index');
  320. $this->assertSame('pre/{locale}', $route->getPrefix());
  321. }
  322. public function testGroupGenerateNameForDuplicateRouteNamesThatEndWithDot()
  323. {
  324. $this->routeCollection->add($this->newRoute('GET', 'foo', ['uses' => 'FooController@index'])->name('foo.'));
  325. $this->routeCollection->add($route = $this->newRoute('GET', 'bar', ['uses' => 'BarController@index'])->name('foo.'));
  326. $routes = $this->collection();
  327. $this->assertSame('BarController@index', $routes->match(Request::create('/bar', 'GET'))->getAction()['uses']);
  328. }
  329. public function testRouteBindingsAreProperlySaved()
  330. {
  331. $this->routeCollection->add($this->newRoute('GET', 'posts/{post:slug}/show', [
  332. 'uses' => 'FooController@index',
  333. 'prefix' => 'profile/{user:username}',
  334. 'as' => 'foo',
  335. ]));
  336. $route = $this->collection()->getByName('foo');
  337. $this->assertSame('profile/{user}/posts/{post}/show', $route->uri());
  338. $this->assertSame(['user' => 'username', 'post' => 'slug'], $route->bindingFields());
  339. }
  340. public function testMatchingSlashedRoutes()
  341. {
  342. $this->routeCollection->add(
  343. $route = $this->newRoute('GET', 'foo/bar', ['uses' => 'FooController@index', 'as' => 'foo'])
  344. );
  345. $this->assertSame('foo', $this->collection()->match(Request::create('/foo/bar/'))->getName());
  346. }
  347. public function testMatchingUriWithQuery()
  348. {
  349. $this->routeCollection->add(
  350. $route = $this->newRoute('GET', 'foo/bar', ['uses' => 'FooController@index', 'as' => 'foo'])
  351. );
  352. $this->assertSame('foo', $this->collection()->match(Request::create('/foo/bar/?foo=bar'))->getName());
  353. }
  354. public function testMatchingRootUri()
  355. {
  356. $this->routeCollection->add(
  357. $route = $this->newRoute('GET', '/', ['uses' => 'FooController@index', 'as' => 'foo'])
  358. );
  359. $this->assertSame('foo', $this->collection()->match(Request::create('http://example.com'))->getName());
  360. }
  361. public function testTrailingSlashIsTrimmedWhenMatchingCachedRoutes()
  362. {
  363. $this->routeCollection->add(
  364. $this->newRoute('GET', 'foo/bar', ['uses' => 'FooController@index', 'as' => 'foo'])
  365. );
  366. $request = Request::create('/foo/bar/');
  367. // Access to request path info before matching route
  368. $request->getPathInfo();
  369. $this->assertSame('foo', $this->collection()->match($request)->getName());
  370. }
  371. public function testRouteWithSamePathAndSameMethodButDiffDomainNameWithOptionsMethod()
  372. {
  373. $routes = [
  374. 'foo_domain' => $this->newRoute('GET', 'same/path', [
  375. 'uses' => 'FooController@index',
  376. 'as' => 'foo',
  377. 'domain' => 'foo.localhost',
  378. ]),
  379. 'bar_domain' => $this->newRoute('GET', 'same/path', [
  380. 'uses' => 'BarController@index',
  381. 'as' => 'bar',
  382. 'domain' => 'bar.localhost',
  383. ]),
  384. 'no_domain' => $this->newRoute('GET', 'same/path', [
  385. 'uses' => 'BarController@index',
  386. 'as' => 'no_domain',
  387. ]),
  388. ];
  389. $this->routeCollection->add($routes['foo_domain']);
  390. $this->routeCollection->add($routes['bar_domain']);
  391. $this->routeCollection->add($routes['no_domain']);
  392. $expectedMethods = [
  393. 'OPTIONS',
  394. ];
  395. $this->assertSame($expectedMethods, $this->collection()->match(
  396. Request::create('http://foo.localhost/same/path', 'OPTIONS')
  397. )->methods);
  398. $this->assertSame($expectedMethods, $this->collection()->match(
  399. Request::create('http://bar.localhost/same/path', 'OPTIONS')
  400. )->methods);
  401. $this->assertSame($expectedMethods, $this->collection()->match(
  402. Request::create('http://no.localhost/same/path', 'OPTIONS')
  403. )->methods);
  404. $this->assertEquals([
  405. 'HEAD' => [
  406. 'foo.localhostsame/path' => $routes['foo_domain'],
  407. 'bar.localhostsame/path' => $routes['bar_domain'],
  408. 'same/path' => $routes['no_domain'],
  409. ],
  410. 'GET' => [
  411. 'foo.localhostsame/path' => $routes['foo_domain'],
  412. 'bar.localhostsame/path' => $routes['bar_domain'],
  413. 'same/path' => $routes['no_domain'],
  414. ],
  415. ], $this->collection()->getRoutesByMethod());
  416. }
  417. /**
  418. * Create a new Route object.
  419. *
  420. * @param array|string $methods
  421. * @param string $uri
  422. * @param mixed $action
  423. * @return \Illuminate\Routing\Route
  424. */
  425. protected function newRoute($methods, $uri, $action)
  426. {
  427. return (new Route($methods, $uri, $action))
  428. ->setRouter($this->router)
  429. ->setContainer($this->app);
  430. }
  431. /**
  432. * Create a new fallback Route object.
  433. *
  434. * @param mixed $action
  435. * @return \Illuminate\Routing\Route
  436. */
  437. protected function fallbackRoute($action)
  438. {
  439. $placeholder = 'fallbackPlaceholder';
  440. return $this->newRoute(
  441. 'GET', "{{$placeholder}}", $action
  442. )->where($placeholder, '.*')->fallback();
  443. }
  444. }