# 路由
漂亮的URL對于任何嚴謹的Web應用來說,都是必需的。也就是像`index.php?article_id=57`
這樣的鏈接將會轉化成類似`/read/intro-to-symfony`這樣的鏈接。
富有靈活性也會重要。如果您想把`/blog`改成`/news`,您需要搜索多少這樣的鏈接并逐一更改他們?如果使用了Symonfy的路由,這點就變得很簡單。
Symonfy的路由可以讓您定義有創意的URL到應用的不同地方,在本章結束時,您將能夠:
1. 創建映射到控制器的復雜路由。
2. 在控制器和模板中生成URL。
3. 從Bundles(或其他地方)加載路由資源。
4. 調試路由。
# 路由示例
*路由*是URL到控制器的映射。例如,假設你想匹配`/blog/my-post` 或 `/blog/all-about-symfony`這樣的鏈接到控制器,使控制器能夠查找和渲染博客示例。這個路由很簡單:
注釋配置
~~~
// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\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'
? ? ? ? // ...
? ? }
}
~~~
YAML配置
~~~
# app/config/routing.yml
blog_list:
? ? path: ? ? ?/blog
? ? defaults: ?{ _controller: AppBundle:Blog:list }
blog_show:
? ? path: ? ? ?/blog/{slug}
? ? defaults: ?{ _controller: AppBundle:Blog:show }
~~~
XML
~~~
<!-- 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>
~~~
PHP
~~~
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add('blog_list', new Route('/blog', array(
? ? '_controller' => 'AppBundle:Blog:list',
)));
$collection->add('blog_show', new Route('/blog/{slug}', array(
? ? '_controller' => 'AppBundle:Blog:show',
)));
return $collection;
~~~
根據這兩條路由:
1. 如果用戶進入`/blog`,第一個路由匹配并且執行`listAction()`。
2. 如果用戶進入`/blog/*`,第二個路由匹配,并且執行`showAction()`,因為路由路徑是`/blog/{slug}`,變量`{slug}`是傳入`showAction()`的值。例如,如果用戶進入`/blog/yay-routing`,那么`$slug`就是`yay-routing`。
無論什么時候你在路由路徑中添加`{placeholder}`,這部分就會成為一個占位符,它可以匹配任何值。您的控制器現在也可以聲明一個名為`$placeholder`(占位符名和參數名*必須*匹配)的參數。
每個路由也有一個內部名:`blog_list`和`blog_show`。它們可以是任何字符串,只要他們唯一并且沒有被使用過。稍后,您就可以使用它來生成URL了。
# 其他格式的路由
上面每個方法上的@Route被稱為注釋。如果您想通過YAML、XML或者PHP配置路由,沒問題!
在這些個格式中,`_controller` 的"defaults"是個特殊的鍵,它告訴Symfony路由匹配的是哪個控制器。`_controller`被稱作邏輯名,它遵從指向道特定PHP類方法的模式,在這個例子中`AppBundle\Controller\BlogController::listAction`和`AppBundle\Controller\BlogController::showAction`就是這種模式。
這就是Symfony路由的目的:映射請求路由到一個控制器,按這個宗旨,您將會學到簡單方便映射復雜URL的各種技巧。
# 使占位符必選
假設`blog_list`路由包含博客列表分頁,第二頁URL是`/blog/2`,第三頁是`/blog/3`。如果你想把路由地址改為`/blog/{page}`,您就會遇到一個問題:
* *blog_list*: /blog/{page}會匹配/blog/*;
* *blog_show:* /blog{slug}也會匹配/blog/*。
當兩個路由匹配同個URL時,第一個路由會勝出。不幸的是,這意味著/blog/yay-routing將會匹配blog_list,這就不太好了。
為了解決這個問題,請添加{page}占位符的必選性以使它只匹配數字
注釋配置
~~~
// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\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)
? ? {
? ? ? ? // ...
? ? }
}
~~~
YAML配置
~~~
# app/config/routing.yml
blog_list:
? ? path: ? ? ?/blog/{page}
? ? defaults: ?{ _controller: AppBundle:Blog:list }
? ? requirements:
? ? ? ? page: '\d+'
blog_show:
? ? # ...
~~~
XML配置
~~~
<!-- 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>
~~~
PHP配置
~~~
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add('blog_list', new Route('/blog/{page}', array(
'_controller' => 'AppBundle:Blog:list',
), array(
'page' => '\d+'
)));
// ...
return $collection;
~~~
`\d+`是匹配任意長度數字的正則表達式,現在
| URL |路由 |參數 |
| --- | --- | --- |
| `/blog/2` | `blog_list` | `$page` = `2` |
| `/blog/yay-routing` | `blog_show` | `$slug` = `yay-routing` |
學習更多路由必選項,譬如HTTP方法,域名,動態表達式,請參看原文:[如何定義路由必選項](http://symfony.com/doc/current/routing/requirements.html)
# 給占位符默認值
在之前的例子里,`blog_list`路由有`/blog/{page}`的路由路徑。如果用戶訪問`/blog/1`,它就會匹配。但是,
倘若訪問`/blog`,它就不會匹配。只要你添加占位符,它就必須有個值。
那么,如果想讓用戶訪問`/blog`也能匹配路由`blog_list`該怎么辦呢?通過添加默認值:
注釋配置
~~~
// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class BlogController extends Controller
{
? ? /**
? ? * @Route("/blog/{page}", name="blog_list", requirements={"page": "\d+"})
? ? */
? ? public function listAction($page = 1)
? ? {
? ? ? ? // ...
? ? }
}
~~~
YAML配置
~~~
# app/config/routing.yml
blog_list:
? ? path: ? ? ?/blog/{page}
? ? defaults: ?{ _controller: AppBundle:Blog:list, page: 1 }
? ? requirements:
? ? ? ? page: '\d+'
blog_show:
? ? # ...
~~~
XML配置
~~~
<!-- 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>
~~~
PHP配置
~~~
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add('blog_list', new Route(
'/blog/{page}',
array(
'_controller' => 'AppBundle:Blog:list',
'page' => 1,
),
array(
'page' => '\d+'
)
));
// ...
return $collection;
~~~
現在,當用戶訪問`/blog`,路由`blog_lis`t就會匹配并且`$page`的默認值就是1。
# 高級路由示例
請思考以下示例
注釋配置
~~~
// 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)
? ? {
? ? }
}
~~~
YAML配置
~~~
# 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+
~~~
XML配置
~~~
<!-- 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>
~~~
PHP配置
~~~
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add(
'article_show',
new Route('/articles/{_locale}/{year}/{slug}.{_format}', array(
'_controller' => 'AppBundle:Article:show',
'_format' => 'html',
), array(
'_locale' => 'en|fr',
'_format' => 'html|rss',
'year' => '\d+',
))
);
return $collection;
~~~
正如你所想,只有`{locale}`是`en`或`fr`并且`{year}`是個數字時,路由才會匹配。這個路由也展示了如何在占位符之間使用后綴符號`.`而不是`/`,路由會匹配這些URLs:
* `/articles/en/2010/my-post`
* `/articles/fr/2010/my-post.rss`
* `/articles/en/2013/my-latest-post.html`
## 特殊的路由參數`_format`
這個例子也高亮顯示了特殊的路由參數*_format*,當使用這個參數的時候,匹配的值將會成為請求對象的請求格式。
最終,請求格式被用在響應數據的`Content-Type`中,比如,json請求格式被轉化成`application/json`,它也可以用在控制器中,方便根據_format的值渲染不同模板,`_format`參數在渲染相同內容到不同格式這一方面是十分強大的。
在Symfony3.0之前,通過添加名為`_format`的查詢參數(例如`/foo/bar?_format=json`),可以重寫請求格式。盡管這并不是什么壞習慣,但在Symfony3中請不要這樣做。
**備注**
有時您想全局配置路由部分內容,Symfony提供了通過服務容器參數配置來實現這種要求,請在[如何在路由中使用服務容器](http://symfony.com/doc/current/routing/service_container_parameters.html)章節中了解更多。
**注意**
路由占位符名字不能以數字開頭,長度不得大于**32**個字符
# 特殊路由參數
如你所見,每個路由參數或者默認值事實上都是可以在控制器方法中作為一個參數。另外,有四個參數是特殊的:
每個都會為應用添加唯一的功能。
`_controller`
文如其名,這個參數用來路由匹配時調用的控制器。
`_format`
用來設置請求格式。
`_fragment`
用來設置錨點標識符(URL最后部分以#開頭的可選部分,用來定義文檔某一部分)。
版本3.2新功能。
`_locale`
用來設置請求的語言
# 控制器命名模式
如果你使用YAML,XML或者PHP路由配置,那么每個路由都必須有一個`_controller`參數用以標識哪個控制器在路由匹配時執行。這個參數使用一個被稱為邏輯控制名的字符串模式,Symonfy會用它映射到特定的PHP方法和類上。
這個模式有三部分,每部分用英文冒號分開
`bundle:controller:action`
例如,`_controller`的值是`AppBundle:Blog:show`那么:
| Bundle | 控制器類 | 方法名 |
| --- | --- | --- |
| AppBundle | BlogController | showAction() |
而配置則是
~~~
// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
? ? public function showAction($slug)
? ? {
? ? ? ? // ...
? ? }
}
~~~
**注意**
Symfony會自動添加`Controller`字符串到控制器名中(`Blog`=>`BlogController`),而方法名則會自動添加`Action`(`show`=>`showAction`)。你也可以通過類完整名和方法名來指向這個控制器:`AppBundle\Controller\BlogController::showAction()`,但是習慣上邏輯名稱會更方便和靈活。
**注意**
除了使用邏輯名或完整類名時,Symfony還支持第三種方法指向控制器。這個方法只使用一個`:`來分割,例如
`service_name:indexAction`,它將會指向到一個已經是`服務`的控制器。
# 加載路由
Symfony從一個路由配置文件(`app/config/routing.yml`)中加載應用的所有配置。但是,在這個文件里,你還可以加載任何你需要的路由文件,事實上,默認情況下,Symfony從你的AppBundle文件夾中的`Controller/`中加載注釋路由配置。
YAML配置
~~~
# app/config/routing.yml
app:
? ? resource: "@AppBundle/Controller/"
? ? type: ? ? annotation
~~~
XML配置
~~~
<!-- 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>
~~~
PHP配置
~~~
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
$collection = new RouteCollection();
$collection->addCollection(
// second argument is the type, which is required to enable
// the annotation reader for this resource
$loader->import("@AppBundle/Controller/", "annotation")
);
return $collection;
~~~
# 生成URL
路由系統應當能夠生成URL。現實中,路由是個雙向系統:映射URL到控制器,路由到一個URL。為了生成一個URL,你需要定義路由的名字(例如,`blog_show`),和所有的路由占位符參數(例如`slug` = `my-blog-pos`t)。這樣的話,任何URL都可以很容易的生成。
~~~
class MainController extends Controller
{
? ? public function showAction($slug)
? ? {
? ? ? ? // ...
? ? ? ? // /blog/my-blog-post
? ? ? ? $url = $this->generateUrl(
? ? ? ? ? ? 'blog_show',
? ? ? ? ? ? array('slug' => 'my-blog-post')
? ? ? ? );
? ? }
}
~~~
**注意**
在基礎類`Controller`中定義的generateUrl()方法是以下代碼的整理:
~~~
$url = $this->container->get('router')->generate(
'blog_show',
array('slug' => 'my-blog-post')
);
~~~
# 生成帶有查詢字符串的URL
`generate()`方法是用占位符參數值的數組來生成URI,如果你傳入另外一個參數,那么它將會作為URI的查詢參數來生成URI。
~~~
$this->get('router')->generate('blog', array(
'page' => 2,
'category' => 'Symfony'
));
// /blog/2?category=Symfony
~~~
// /blog/2?category=Symfony
# 在模板中生成URL
在Twig中生成URL,請參看模板章節:[鏈接到頁面](http://symfony.com/doc/current/templating.html#templating-pages),如果想在Javascript中生成URL,請參看[如何在Javascript生成路由URL](http://symfony.com/doc/current/routing/generate_url_javascript.html)章節。
# 生成絕對路徑URL
默認情況下,路由系統生成的是相對路徑URL。在控制器中,傳遞給`generateUrl()`第三個參數`UrlGenerateInterface::ABSOLUTE_URL`,那么就可以生成絕對路徑的URL:
~~~
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
$this->generateUrl('blog_show', array('slug' => 'my-blog-post'), UrlGeneratorInterface::ABSOLUTE_URL);
// http://www.example.com/blog/my-blog-post
~~~
**注意**
生成絕對路徑URL的主機名會通過當前Resquest對象自動探測。當在Web外部生成這些URL(例如在控制臺命令中生成),就
不會生效。請查看[如何在控制臺生成URL](http://symfony.com/doc/current/console/request_context.html)章節了解如何解決這個問題。
# 排憂解難
以下是使用路由時常見的幾個錯誤:
* 控制器AppBundleControllerBlogController::showAction要求你提供為$slug參數提供一個值。
這通常發生在當你的控制器方法有這個參數(例如$slug)
~~~
public function showAction($slug)
{
? ? // ..
}
~~~
但是你的路由并沒有`{slug}`這個占位符(例如它是`/blog/show`)。添加{slug}到你的路由路徑:`/blog/show/{slug}`,或者給參數一個默認值(例如 `$slug = null`)。
* 路由"blog_show"生成URL需要某些強制性的參數(例如‘slug’)缺失。
也就是說你想為blog_show路由生成URL,但你沒有傳遞slug占位符的值,因為它是必需的。解決這個問題,請傳遞slug一個值。
~~~
$this->generateUrl('blog_show', array('slug' => 'slug-value'));
// 或者在Twig中
// {{ path('blog_show', {'slug': 'slug-value'}) }}
~~~
# 總結
路由時個映射請求到處理請求的控制器方法的一個系統。它允許你制作漂亮的URL并且是你的應用功能和這些URL解耦。路由采用了雙向機制,因此你也可以使用它來生成URL。