Routing
對任何專業的網頁來說,有個漂亮的網址是絕對必須的。這代表說拋棄像是 index.php?article_id=57
的醜陋網址吧,改用這樣的網址 /read/intro-to-symfony
。
保有彈性是更重要的,如果你想要將頁面的網址從 /blog
改成 /news
,你會有多少連結需要被找到並修改他呢?如果你是用 Symfony 的 router,修改網址是很簡單的。
Symfony router 讓你可以定義有創意的網址,並對應到你的網頁應用中的不同區塊。在讀完本篇文章之後,你可以做到以下幾件事:
- 建立複雜的 route,並對應到相關的 controller
- 在 template 或 controller 中產生網址字串
- 從 bundle (或其他方式) 載入 routing 資訊
- 對你的 route 除錯
Routing 範例
一個 route 代表的是一個網址和一個 controller 之間的對應關係。舉個例子,假設你想在 /blog/my-post
或 /blog/all-about-symfony
網址中,在其對應的 controller 中查詢並顯示部落格的文章。這樣的 route 設定舉例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends Controller
{
/**
* Matches /blog exactly
*
* @Route("/blog", name="blog_list")
*/
public function listAction()
{
// ...
}
/**
* Matches /blog/*
*
* @Route("/blog/{slug}", name="blog_show")
*/
public function showAction($slug)
{
// $slug will equal the dynamic part of the URL
// e.g. at /blog/yay-routing, then $slug='yay-routing'
// ...
}
}
|
1 2 3 4 5 6 7 8 | # app/config/routing.yml
blog_list:
path: /blog
defaults: { _controller: AppBundle:Blog:list }
blog_show:
path: /blog/{slug}
defaults: { _controller: AppBundle:Blog:show }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog_list" path="/blog">
<default key="_controller">AppBundle:Blog:list</default>
</route>
<route id="blog_show" path="/blog/{slug}">
<default key="_controller">AppBundle:Blog:show</default>
</route>
</routes>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 | // app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$routes = new RouteCollection();
$routes->add('blog_list', new Route('/blog', [
'_controller' => 'AppBundle:Blog:list',
]));
$routes->add('blog_show', new Route('/blog/{slug}', [
'_controller' => 'AppBundle:Blog:show',
]));
return $routes;
|
Thanks to these two routes: 因為有這兩個 route:
- 如果使用者訪問
/blog
,第一個 route 會命中並執行listAction()
的方法。 - 如果使用者訪問
/blog/*
,第二個 route 會命中並執行showAction()
的方法,因為 route 的路徑是設定為/blog/{slug}
,$slug
這個參數會被傳遞至showAction()
的方法內。舉個例子,如果使用者訪問/blog/yay-routing
,那$slug
就會是yay-routing
。
每當你的 route 路徑含有 {placeholder}
的部分,這個部分會變成一個萬用字元:他會匹配到 任何 文字。你的 controller 也 可以包含一個 $placeholder
參數 (萬用字元和參數名稱 必須 要相同)。
然而,{placeholder}
裡的斜線 /
預設會被忽略,因為 router 會用斜線分隔不同的 placeholder。想了解更多,可以閱讀 How to Allow a "/" Character in a Route Parameter.
每個 route 也有個名字:blog_list
和 blog_show
,可以是任何名稱(只要每個名稱是唯一的就好),接下來你會用它產生網址。
Symfony router 的目的是建立網址和 controller 的關聯。在這過程中,你會習得各種建立 route 的技巧,就算是複雜的網址也可以很簡單。
加入 {wildcard} 的限制條件 (requirements
)
假設 blog_list
的 route 顯示一個有分頁的部落格文章清單,網址像是 /blog/2
和 /blog/3
表示第2頁和第3頁。如果你設定 route 的路徑為 /blog/{page}
,你會遇到一個問題:
- blog_list:
/blog/{page}
會比對為/blog/*
- blog_show:
/blog/{slug}
會比對為/blog/*
當兩個 route 對應到一樣的網址,第一個被載入的 route 會勝出 (比對成功),也就是說 /blog/yay-routing
會比對到 blog_list
的 route,這不太妙喔!
要修正這個問題,加入一個 requirements
設定,讓 {page}
只允許數字的內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends Controller
{
/**
* @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
*/
public function listAction($page)
{
// ...
}
/**
* @Route("/blog/{slug}", name="blog_show")
*/
public function showAction($slug)
{
// ...
}
}
|
1 2 3 4 5 6 7 8 9 | # app/config/routing.yml
blog_list:
path: /blog/{page}
defaults: { _controller: AppBundle:Blog:list }
requirements:
page: '\d+'
blog_show:
# ...
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog_list" path="/blog/{page}">
<default key="_controller">AppBundle:Blog:list</default>
<requirement key="page">\d+</requirement>
</route>
<!-- ... -->
</routes>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$routes = new RouteCollection();
$routes->add('blog_list', new Route('/blog/{page}', [
'_controller' => 'AppBundle:Blog:list',
], [
'page' => '\d+'
]));
// ...
return $routes;
|
\d+
是正規表示式,會比對出一個任意長度的數字,結果如下:
URL | Route | Parameters |
---|---|---|
/blog/2 | blog_list | $page = 2 |
/blog/yay-routing | blog_show | $slug = yay-routing |
想了解更多其他 requirements
的用法,像是 HTTP method、hostname、和 dynamic expressions,請見 How to Define Route Requirements。
給定 {placeholders} 一個預設值
在之前的範例中,blog_list
的網址路徑為 /blog/{page}
。如果用戶訪問 /blog/1
那就會匹配到該 route,但如果訪問 /blog
的話,則會無法匹配。只要你的 route 包含一個 {placeholder}
,那他必須要有個值。
那要如何在訪問 /blog
時也能被匹配到 blog_list
呢?我們需要加一個預設值上去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends Controller
{
/**
* @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
*/
public function listAction($page = 1)
{
// ...
}
}
|
1 2 3 4 5 6 7 8 9 | # app/config/routing.yml
blog_list:
path: /blog/{page}
defaults: { _controller: AppBundle:Blog:list, page: 1 }
requirements:
page: '\d+'
blog_show:
# ...
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog_list" path="/blog/{page}">
<default key="_controller">AppBundle:Blog:list</default>
<default key="page">1</default>
<requirement key="page">\d+</requirement>
</route>
<!-- ... -->
</routes>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$routes = new RouteCollection();
$routes->add('blog_list', new Route(
'/blog/{page}',
[
'_controller' => 'AppBundle:Blog:list',
'page' => 1,
],
[
'page' => '\d+'
]
));
// ...
return $routes;
|
這樣的話,當用戶訪問 /blog
時 blog_list
的 route 將會被匹配到,並且 $page
預設的數值為 1
。
進階的 Routing 範例
記住所有前述的資訊,來看看這個進階的範例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // src/AppBundle/Controller/ArticleController.php
// ...
class ArticleController extends Controller
{
/**
* @Route(
* "/articles/{_locale}/{year}/{slug}.{_format}",
* defaults={"_format": "html"},
* requirements={
* "_locale": "en|fr",
* "_format": "html|rss",
* "year": "\d+"
* }
* )
*/
public function showAction($_locale, $year, $slug)
{
}
}
|
1 2 3 4 5 6 7 8 | # app/config/routing.yml
article_show:
path: /articles/{_locale}/{year}/{slug}.{_format}
defaults: { _controller: AppBundle:Article:show, _format: html }
requirements:
_locale: en|fr
_format: html|rss
year: \d+
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="article_show"
path="/articles/{_locale}/{year}/{slug}.{_format}">
<default key="_controller">AppBundle:Article:show</default>
<default key="_format">html</default>
<requirement key="_locale">en|fr</requirement>
<requirement key="_format">html|rss</requirement>
<requirement key="year">\d+</requirement>
</route>
</routes>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$routes = new RouteCollection();
$routes->add(
'article_show',
new Route('/articles/{_locale}/{year}/{slug}.{_format}', [
'_controller' => 'AppBundle:Article:show',
'_format' => 'html',
], [
'_locale' => 'en|fr',
'_format' => 'html|rss',
'year' => '\d+',
])
);
return $routes;
|
就像你看到的,該 route 只會在 {_locale}
為 en
或 fr
並且 {year}
是一個數字的情形下匹配成功,該 route 同時也告訴你可以在 {placeholder} 之間使用小數點 (.
) 來取代斜線 (/
),匹配該 route 的網址有以下幾個可能:
/articles/en/2010/my-post
/articles/fr/2010/my-post.rss
/articles/en/2013/my-latest-post.html
有時候你想要讓 route 的某部分能被設定在全域環境中,Symfony 中可以設定 service container 的參數來達到這件事,詳細可以閱讀「How to Use Service Container Parameters in your Routes」。
特殊 Routing 參數
就像你看到的,每個 routing 參數或是預設值,最終都可以用在 controller method 中的參數上。此外,還有 4 個特殊的參數,每個都有特定的功能:
_controller
正如你所見,當 route 被匹配後,該參數用於決定要執行哪個 controller。_format
用於設定請求格式 (read more)_fragment
用於設定 fragment 錨點,網址中最尾巴以#
符號開始的部分,用於識別網頁中的某個部分。`_fragment` 參數功能加入於 Symfony 3.2。
_locale
用於設定語系 (read more)。
帶有結尾斜線的重新導向網址
從歷史的腳色來看,網址是遵從 UNIX 的慣例,在結尾處加入斜線作為資料夾 (像是 https://example.com/foo/
),在結尾處移除斜線作為檔案 (https://example.com/foo
)。雖然要將兩種網址指向不同的內容是 OK 的,但現今將兩種網址指向同一個內容或是做重新導向已經變為一種共識了。
Symfony 遵從這樣的邏輯,重新導向有或是沒有結尾斜線的網址 (但只限於 GET
和 HEAD
請求):
Route path | 如果請求網址為 /foo | 如果請求網址為 /foo/ |
---|---|---|
/foo |
匹配成功 (200 status response) |
匹配不成功 (404 status response) |
/foo/ |
301 重新導向到 /foo/ |
匹配成功 (200 status response) |
總結來說,在 route path 中加入結尾斜線是確保網址能運作正常的最佳作法,閱讀 Redirect URLs with a Trailing Slash 的文章,學習如何避免在請求網址包含結尾的斜線但 route path 沒有時發生的 404
錯誤。
Controller Naming Pattern
If you use YAML, XML or PHP route configuration, then each route must have a _controller
parameter, which dictates which controller should be executed when that route is matched. This parameter uses a simple string pattern called the logical controller name, which Symfony maps to a specific PHP method and class. The pattern has three parts, each separated by a colon:
bundle:controller:action
For example, a _controller
value of AppBundle:Blog:show
means:
Bundle | Controller Class | Method Name |
---|---|---|
AppBundle | BlogController | showAction() |
The controller might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 | // src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function showAction($slug)
{
// ...
}
}
|
Notice that Symfony adds the string Controller
to the class name (Blog
=> BlogController
) and Action
to the method name (show
=> showAction()
).
You could also refer to this controller using its fully-qualified class name and method: AppBundle\Controller\BlogController::showAction
. But if you follow some simple conventions, the logical name is more concise and allows more flexibility.
To refer to an action that is implemented as the __invoke()
method of a controller class, you do not have to pass the method name, but can just use the fully qualified class name (e.g. AppBundle\Controller\BlogController
).
In addition to using the logical name or the fully-qualified class name, Symfony supports a third way of referring to a controller. This method uses just one colon separator (e.g. service_name:indexAction
) and refers to the controller as a service (see How to Define Controllers as Services).
Loading Routes
Symfony loads all the routes for your application from a single routing configuration file: app/config/routing.yml
. But from inside of this file, you can load any other routing files you want. In fact, by default, Symfony loads annotation route configuration from your AppBundle's Controller/
directory, which is how Symfony sees our annotation routes:
1 2 3 4 | # app/config/routing.yml
app:
resource: "@AppBundle/Controller/"
type: annotation
|
1 2 3 4 5 6 7 8 9 10 | <!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<!-- the type is required to enable the annotation reader for this resource -->
<import resource="@AppBundle/Controller/" type="annotation"/>
</routes>
|
1 2 3 4 5 6 7 8 9 10 11 | // app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
$routes = new RouteCollection();
$routes->addCollection(
// second argument is the type, which is required to enable
// the annotation reader for this resource
$loader->import("@AppBundle/Controller/", "annotation")
);
return $routes;
|
For more details on loading routes, including how to prefix the paths of loaded routes, see How to Include External Routing Resources.
Generating URLs
The routing system should also be used to generate URLs. In reality, routing is a bidirectional system: mapping the URL to a controller and a route back to a URL.
To generate a URL, you need to specify the name of the route (e.g. blog_show
) and any wildcards (e.g. slug = my-blog-post
) used in the path for that route. With this information, any URL can easily be generated:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class MainController extends Controller
{
public function showAction($slug)
{
// ...
// /blog/my-blog-post
$url = $this->generateUrl(
'blog_show',
['slug' => 'my-blog-post']
);
}
}
|
The generateUrl()
method defined in the base Controller class is just a shortcut for this code:
1 2 3 4 | $url = $this->container->get('router')->generate(
'blog_show',
['slug' => 'my-blog-post']
);
|
Generating URLs with Query Strings
The generate()
method takes an array of wildcard values to generate the URI. But if you pass extra ones, they will be added to the URI as a query string:
1 2 3 4 5 | $this->get('router')->generate('blog', [
'page' => 2,
'category' => 'Symfony',
]);
// /blog/2?category=Symfony
|
Generating URLs from a Template
To generate URLs inside Twig, see the templating article: Linking to Pages. If you also need to generate URLs in JavaScript, see How to Generate Routing URLs in JavaScript.
Generating Absolute URLs
By default, the router will generate relative URLs (e.g. /blog
). From a controller, pass UrlGeneratorInterface::ABSOLUTE_URL
to the third argument of the generateUrl()
method:
1 2 3 4 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
$this->generateUrl('blog_show', ['slug' => 'my-blog-post'], UrlGeneratorInterface::ABSOLUTE_URL);
// http://www.example.com/blog/my-blog-post
|
The host that's used when generating an absolute URL is automatically detected using the current Request
object. When generating absolute URLs from outside the web context (for instance in a console command) this doesn't work. See How to Generate URLs from the Console to learn how to solve this problem.
Troubleshooting
Here are some common errors you might see while working with routing:
Controller "AppBundle\Controller\BlogController::showAction()" requires that you provide a value for the "$slug" argument.
This happens when your controller method has an argument (e.g. $slug
):
1 2 3 4 | public function showAction($slug)
{
// ..
}
|
But your route path does not have a {slug}
wildcard (e.g. it is /blog/show
). Add a {slug}
to your route path: /blog/show/{slug}
or give the argument a default value (i.e. $slug = null
).
Some mandatory parameters are missing ("slug") to generate a URL for route "blog_show".
This means that you're trying to generate a URL to the blog_show
route but you are not passing a slug
value (which is required, because it has a {slug}
) wildcard in the route path. To fix this, pass a slug
value when generating the route:
1 2 3 4 | $this->generateUrl('blog_show', ['slug' => 'slug-value']);
// or, in Twig
// {{ path('blog_show', {'slug': 'slug-value'}) }}
|
Translating Routes
Symfony doesn't support defining routes with different contents depending on the user language. In those cases, you can define multiple routes per controller, one for each supported language; or use any of the bundles created by the community to implement this feature, such as JMSI18nRoutingBundle and BeSimpleI18nRoutingBundle.
Summary
Routing is a system for mapping the URL of incoming requests to the controller function that should be called to process the request. It both allows you to specify beautiful URLs and keeps the functionality of your application decoupled from those URLs. Routing is a bidirectional mechanism, meaning that it should also be used to generate URLs.
Keep Going!
Routing, check! Now, uncover the power of controllers.
此篇文章主要翻譯自 Symfony 官方文件,並做了些許的修改,原始文章請見此連結