# 路由
漂亮的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。