路由

对于任何严谨的web应用程序而言美观的URL是绝对必须的。这意味着日渐淘汰的 index.php?article_id=57 这类丑陋的URL要被 /read/intro-to-symfony 取代。

拥有灵活性是更加重要的。你把页面的URL从 /blog 改为 /news 时需要做些什么?你需要追踪并更新多少链接,才能做出这种改变?如果你使用Symfony的路由,改变起来很容易。

Symfony路由器允许你定义创造性的url,再将其映射到程序不同区域。读完本文,你可以做到:

  • 创建复杂的路由,将其映射到控制器

  • 在模板和控制器中生成URL

  • 从Bundle中(或从其他地方)加载路由资源

  • 对路由除错

路由示例

一个 路由,是指一个URL路径(path)到一个控制器(controller)的映射。例如,你想(通过路由)匹配到诸如 /blog/my-post/blog/all-about-symfony 这样的任何一个URL,并且把路由发送到一个“能够查询和输出该篇博文”的控制器。这个路由很简单:

Annotations

  1. // src/AppBundle/Controller/BlogController.php
  2. namespace AppBundle\Controller;
  3.  
  4. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  6.  
  7. class BlogController extends Controller
  8. {
  9. /**
  10. * Matches /blog exactly / 精确匹配了/blog
  11. *
  12. * @Route("/blog", name="blog_list")
  13. */
  14. public function listAction()
  15. {
  16. // ...
  17. }
  18.  
  19. /**
  20. * Matches /blog/* / 匹配的是/blog/*
  21. *
  22. * @Route("/blog/{slug}", name="blog_show")
  23. */
  24. public function showAction($slug)
  25. {
  26. // $slug will equal the dynamic part of the URL
  27. // e.g. at /blog/yay-routing, then $slug='yay-routing'
  28. // $slug 必须等同于URL中的动态部分
  29. // 即,在 /blog/yay-routing 中 $slug='yay-routing'
  30.  
  31. // ...
  32. }
  33. }
YAML
  1. # app/config/routing.yml
  2. blog_list:
  3. path: /blog
  4. defaults: { _controller: AppBundle:Blog:list }
  5. blog_show:
  6. path: /blog/{slug}
  7. defaults: { _controller: AppBundle:Blog:show }

XML

  1. <!-- app/config/routing.xml -->
  2. <?xml version="1.0" encoding="UTF-8" ?>
  3. <routes xmlns="http://symfony.com/schema/routing"
  4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5. xsi:schemaLocation="http://symfony.com/schema/routing
  6. http://symfony.com/schema/routing/routing-1.0.xsd">
  7.  
  8. <route id="blog_list" path="/blog">
  9. <default key="_controller">AppBundle:Blog:list</default>
  10. </route>
  11.  
  12. <route id="blog_show" path="/blog/{slug}">
  13. <default key="_controller">AppBundle:Blog:show</default>
  14. </route>
  15. </routes>

PHP

  1. // app/config/routing.php
  2. use Symfony\Component\Routing\RouteCollection;
  3. use Symfony\Component\Routing\Route;
  4.  
  5. $collection = new RouteCollection();
  6. $collection->add('blog_list', new Route('/blog', array(
  7. '_controller' => 'AppBundle:Blog:list',
  8. )));
  9. $collection->add('blog_show', new Route('/blog/{slug}', array(
  10. '_controller' => 'AppBundle:Blog:show',
  11. )));
  12.  
  13. return $collection;

多亏了以下这两个路由:

  • 如果用户来到 /blog,则第一个路由被匹配而 listAction() 会被执行;
  • 如果用户来到 /blog/*, 则第二个路由被匹配而 showAction() 将被执行。因为路由的路径是 /blog/{slug}, 有个 $slug 变量被传到了能够匹配该值的 showAction() 中。例如,如果用户来到 /blog/yay-routing,那么 $slug 即等于 yay-routing。 只要在你的路由路径(route path)中包含有 {placeholder},它便成为一个通配符:它可以匹配 任何 值。你的控制器从现在起 可以有一个名为 $placeholder 的参数(此通配符和参数 必须 匹配)。

每个路由都有一个内部名称: blog_listblog_show。这些名称可以是任何内容 (只要唯一即可) 而且不必赋予任何特别的含意。后面,你将使用它来生成Url。

其他格式的路由

每个方法上面的 @Route 被称为一个 annotation(注释)。如果你希望以YAML, XML 或 PHP来配置你的路由,绝对没问题!

在这些格式中,_controller “默认”值,是一个特殊的键,它告诉Symfony,当一个URL匹配这个路由时,要去执行哪个控制器。_controller 字符串被称为 logical name(逻辑名)。它遵循的是“指向特定的PHP类或方法”这样一个模式,本例中是 AppBundle\Controller\BlogController::listActionAppBundle\Controller\BlogController::showAction 方法。

这就是Symfony路由的目标:把一个请求的URL映射到控制器中。接下来,你将学习到所有类型的技巧,可以令极度复杂的URL在映射时变得容易。

添加{通配符}条件

设想 blog_list 路由将包含博客主题带有分页的一个列表,对于第2和第3页有类似 /blog/2/blog/3 这样的URL。如果你把路由路径改为 /blog/{page},会出现问题:

  • blog_list: /blog/{page} 将匹配 /blog/*;
  • blogshow: /blog/{slug}同样 匹配 /blog/*。 当两个路由匹配同一个URL时,第一个_ 被加载的路由胜出。不幸的是,这也意味着 /blog/yay-routing 将匹配到 blog_list。不太好!

要修复这个,添加一个 requirement(条件),以便 {page} 通配符能够 匹配数字(digits):

Annotations

  1. // src/AppBundle/Controller/BlogController.php
  2. namespace AppBundle\Controller;
  3.  
  4. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  6.  
  7. class BlogController extends Controller
  8. {
  9. /**
  10. * @Route("/blog/{page}", name="blog_list", requirements={"page": "\d+"})
  11. */
  12. public function listAction($page)
  13. {
  14. // ...
  15. }
  16.  
  17. /**
  18. * @Route("/blog/{slug}", name="blog_show")
  19. */
  20. public function showAction($slug)
  21. {
  22. // ...
  23. }
  24. }

YAML

  1. # app/config/routing.yml
  2. blog_list:
  3. path: /blog/{page}
  4. defaults: { _controller: AppBundle:Blog:list }
  5. requirements:
  6. page: '\d+'
  7. blog_show:
  8. # ...

XML

  1. <!-- app/config/routing.xml -->
  2. <?xml version="1.0" encoding="UTF-8" ?>
  3. <routes xmlns="http://symfony.com/schema/routing"
  4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5. xsi:schemaLocation="http://symfony.com/schema/routing
  6. http://symfony.com/schema/routing/routing-1.0.xsd">
  7.  
  8. <route id="blog_list" path="/blog/{page}">
  9. <default key="_controller">AppBundle:Blog:list</default>
  10. <requirement key="page">\d+</requirement>
  11. </route>
  12.  
  13. <!-- ... -->
  14. </routes>
PHP
  1. // app/config/routing.php
  2. use Symfony\Component\Routing\RouteCollection;
  3. use Symfony\Component\Routing\Route;
  4.  
  5. $collection = new RouteCollection();
  6. $collection->add('blog_list', new Route('/blog/{page}', array(
  7. '_controller' => 'AppBundle:Blog:list',
  8. ), array(
  9. 'page' => '\d+'
  10. )));
  11.  
  12. // ...
  13.  
  14. return $collection;

\d+ 是一个正则表达式,匹配了任意长度的 数字。现在:

URL路由参数
/blog/2blog_list$page = 2
/blog/yay-routingblog_show$slug = yay-routing

要了解其他一些路由的条件 - 像是HTTP method, hostname 以及 dynamicexpressions(动态表达式) - 参考 如何定义路由条件

给{占位符}一个默认值

前例中,bloglist 路由的路径是 /blog/{page}。如果用户访问 /blog/1,则会匹配。但如果他们访问的是 /blog,就将 无法 匹配到。一旦你添加了某个 {占位符} 到路由中,它就 必须_ 得有一个值。

那么当用户访问 /blog 时,你如何才能令 bloglist 再一次匹配呢?添加一个 默认_ 值即可:

Annotations

  1. // src/AppBundle/Controller/BlogController.php
  2. namespace AppBundle\Controller;
  3.  
  4. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  6.  
  7. class BlogController extends Controller
  8. {
  9. /**
  10. * @Route("/blog/{page}", name="blog_list", requirements={"page": "\d+"})
  11. */
  12. public function listAction($page = 1)
  13. {
  14. // ...
  15. }
  16. }

YAML

  1. # app/config/routing.yml
  2. blog_list:
  3. path: /blog/{page}
  4. defaults: { _controller: AppBundle:Blog:list, page: 1 }
  5. requirements:
  6. page: '\d+'
  7. blog_show:
  8. # ...

XML

  1. <!-- app/config/routing.xml -->
  2. <?xml version="1.0" encoding="UTF-8" ?>
  3. <routes xmlns="http://symfony.com/schema/routing"
  4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5. xsi:schemaLocation="http://symfony.com/schema/routing
  6. http://symfony.com/schema/routing/routing-1.0.xsd">
  7.  
  8. <route id="blog_list" path="/blog/{page}">
  9. <default key="_controller">AppBundle:Blog:list</default>
  10. <default key="page">1</default>
  11.  
  12. <requirement key="page">\d+</requirement>
  13. </route>
  14.  
  15. <!-- ... -->
  16. </routes>
PHP
  1. // app/config/routing.php
  2. use Symfony\Component\Routing\RouteCollection;
  3. use Symfony\Component\Routing\Route;
  4.  
  5. $collection = new RouteCollection();
  6. $collection->add('blog_list', new Route(
  7. '/blog/{page}',
  8. array(
  9. '_controller' => 'AppBundle:Blog:list',
  10. 'page' => 1,
  11. ),
  12. array(
  13. 'page' => '\d+'
  14. )
  15. ));
  16.  
  17. // ...
  18.  
  19. return $collection;

现在,当用户访问 /blog 时,blog_list 路由会匹配,并且 $page 路由参数会默认取值为 1

高级路由示例

贯穿所有内容,查看下例:

Annotations

  1. // src/AppBundle/Controller/ArticleController.php
  2.  
  3. // ...
  4. class ArticleController extends Controller
  5. {
  6. /**
  7. * @Route(
  8. * "/articles/{_locale}/{year}/{slug}.{_format}",
  9. * defaults={"_format": "html"},
  10. * requirements={
  11. * "_locale": "en|fr",
  12. * "_format": "html|rss",
  13. * "year": "\d+"
  14. * }
  15. * )
  16. */
  17. public function showAction($_locale, $year, $slug)
  18. {
  19. }
  20. }
YAML
  1. # app/config/routing.yml
  2. article_show:
  3. path: /articles/{_locale}/{year}/{slug}.{_format}
  4. defaults: { _controller: AppBundle:Article:show, _format: html }
  5. requirements:
  6. _locale: en|fr
  7. _format: html|rss
  8. year: \d+
XML
  1. <!-- app/config/routing.xml -->
  2. <?xml version="1.0" encoding="UTF-8" ?>
  3. <routes xmlns="http://symfony.com/schema/routing"
  4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5. xsi:schemaLocation="http://symfony.com/schema/routing
  6. http://symfony.com/schema/routing/routing-1.0.xsd">
  7.  
  8. <route id="article_show"
  9. path="/articles/{_locale}/{year}/{slug}.{_format}">
  10.  
  11. <default key="_controller">AppBundle:Article:show</default>
  12. <default key="_format">html</default>
  13. <requirement key="_locale">en|fr</requirement>
  14. <requirement key="_format">html|rss</requirement>
  15. <requirement key="year">\d+</requirement>
  16.  
  17. </route>
  18. </routes>
PHP
  1. // app/config/routing.php
  2. use Symfony\Component\Routing\RouteCollection;
  3. use Symfony\Component\Routing\Route;
  4.  
  5. $collection = new RouteCollection();
  6. $collection->add(
  7. 'article_show',
  8. new Route('/articles/{_locale}/{year}/{slug}.{_format}', array(
  9. '_controller' => 'AppBundle:Article:show',
  10. '_format' => 'html',
  11. ), array(
  12. '_locale' => 'en|fr',
  13. '_format' => 'html|rss',
  14. 'year' => '\d+',
  15. ))
  16. );
  17.  
  18. return $collection;

如你所见,这个路由仅在URL的 {_locale} 部分是 enfr 时,同时 {year} 是一个数字的时候,才会匹配。此路由也表明,你可以在占位符之间使用一个“点”而不是一个斜杠。匹配这个路由的URL可能是下面这种:

  • /articles/en/2010/my-post
  • /articles/fr/2010/my-post.rss
  • /articles/en/2013/my-latest-post.html

这个例子也突显了 _format 路由参数(routing parameter)。当使用这个参数时,匹配到的值将成为 Request 对象中的"request format"(请求format)。

最后,请求格式将被用于某些事情,比如设置响应的 Content-Type 等(如,一个 json 的请求format将转换成 application/jsonContent-Type)。它也可用于控制器,对每一个 _format 值来输出不同的模板。_format 是一种强有力的方式,来在不同格式(format)之间输出相同的内容。

Symfony 3.0之前的版本中,可以通过(URL中的)名为 _format 的请求参数(query parameter),来覆写request format(例如:/foo/bar?_format=json)。依赖此种行为不光被认为是个糟糕实践,而且会影响你的程序升级到Symfony 3。

Note

有时,你希望令路由中的特定部分能够进行全局配置。Symfony提供了一种方式,即利用服务容器参数来实现之。更多内容可参考 "如何在路由中使用容器参数"。

Caution

一个路由占位符名称不可以起于数字,也不可以长于32个字符。

特殊路由参数

你已经看到,每一个路由参数或其默认值最终都是可以做为参数(arguments)用在控制器方法(译注:即action)中的。此外,还有四个特殊参数:每一个都能为你的程序增添一个独有的功能性。

_controller
就像你看到的,此参数用于决定,当路由匹配时,须执行哪个控制器。
_format
用于设置request format (详见这里)。
_fragment
用于设置fragment identifier,即,URL中可选的最末部分,起自一个 # 字符,用来识别页面中的某一部分。 Symfony 3.2: _fragment 参数自Symfony 3.2起被引入。
_locale
用于设置请求中的locale信息 (详见这里)。

控制器命名法

如果你使用YAML, XML 或 PHP方式的路由配置,则每个路由必须有一个 controller 参数,用于指定当路由匹配时要执行哪个控制器。这个参数使用了一个简单的字符串规范,被称为 _logical controller name(逻辑控制器名),Symfony据此(将路由)映射到某个特定的PHP方法或类中。这一命名法有三个部分,每个部分用一个colon(冒号)分隔开来:

bundle:controller:action

例如,AppBundle:Blog:show 这个 _controller 值代表:

Bundle(Bundle名)Controller Class(控制器类名)Method Name(控制器方法名)
AppBundleBlogControllershowAction()

控制器看起来像下面这样:

  1. // src/AppBundle/Controller/BlogController.php
  2. namespace AppBundle\Controller;
  3.  
  4. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  5.  
  6. class BlogController extends Controller
  7. {
  8. public function showAction($slug)
  9. {
  10. // ...
  11. }
  12. }

注意Symfony添加了字符串 Controller 到类名中 (Blog=> BlogController) 同时添加了 Action 到方法名中 (show => showAction())。

你也可以使用类的FQCN来引用类名和方法: AppBundle\Controller\BlogController::showAction。但如果你遵循了是简便方法,逻辑命名是非常轻巧和灵活的。

Tip

若要引用一个在控制器类中实现了 __invoke() 方法的action,你不可以传入方法名,只需使用FQCN类名足矣 (如 AppBundle\Controller\BlogController)。

Note

除了使用逻辑名或FQCN类名之外,Symfony还支持第三种方式来引用控制器。此方法仅使用一个冒号 (如 servicename:indexAction) 把控制器作为服务来引入 (参考 [如何把控制器定义为服务_](http://www.symfonychina.com/doc/current/controller/service.html))。

加载路由

Symfony 从一个 单一的 路由配置文件: app/config/routing.yml中加载你的程序中的全部路由。但从这个文件中,你可以加载任何一个 其他的 路由文件。实际上,Symfony默认从你 AppBundle 中的 Controller/ 目录中加载annotation路由配置,这就是为何Symfony能够看到我们的annotation路由:

  1. # app/config/routing.yml
  2. app:
  3. resource: "@AppBundle/Controller/"
  4. type: annotation
  1. <!-- app/config/routing.xml -->
  2. <?xml version="1.0" encoding="UTF-8" ?>
  3. <routes xmlns="http://symfony.com/schema/routing"
  4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5. xsi:schemaLocation="http://symfony.com/schema/routing
  6. http://symfony.com/schema/routing/routing-1.0.xsd">
  7.  
  8. <!-- the type is required to enable the annotation reader for this resource -->
  9. <import resource="@AppBundle/Controller/" type="annotation"/>
  10. </routes>
  1. // app/config/routing.php
  2. use Symfony\Component\Routing\RouteCollection;
  3.  
  4. $collection = new RouteCollection();
  5. $collection->addCollection(
  6. // second argument is the type, which is required to enable
  7. // the annotation reader for this resource
  8. $loader->import("@AppBundle/Controller/", "annotation")
  9. );
  10.  
  11. return $collection;

路由加载的更多内容,包括如何为被加载的路由施以前缀,参考 如何将外部路由资源包容进来

生成URL

路由系统也可用来生成URL链接。现实中,路由是个双向系统:把URL映射到控制器,或把路由反解为URL。

要生成URL,你需要指定路由名称 (即 blog_show) 以及用在此路由路径中的任意通配符(如 slug = my-blog-post) 的值。有了这些信息,任何URL皆可轻松生成:

  1. class MainController extends Controller
  2. {
  3. public function showAction($slug)
  4. {
  5. // ...
  6.  
  7. // /blog/my-blog-post
  8. $url = $this->generateUrl(
  9. 'blog_show',
  10. array('slug' => 'my-blog-post')
  11. );
  12. }
  13. }

Note

定义在Controller 基类中的 generateUrl() 方法是以下代码的快捷方式:

  1. $url = $this->container->get('router')->generate(
  2. 'blog_show',
  3. array('slug' => 'my-blog-post')
  4. );

生成带有Query字符串的URL

generate() 方法接收通配符的值的数组,以生成URI。但如果你传入额外的值,它们将被添加到URI中,作为query string(查询字符串):

  1. $this->get('router')->generate('blog', array(
  2. 'page' => 2,
  3. 'category' => 'Symfony'
  4. ));
  5. // /blog/2?category=Symfony

从模板中生成URL

在Twig中生成URL,参考模板文章: 链到页面。如果你需要在JavaScript中生成URL,参考 如何在JavaScript中生成路由链接

生成绝对URL

默认时,路由生成相对链接 (如 /blog)。在控制器中把 UrlGeneratorInterface::ABSOLUTE_URL 作为 generateUrl()方法的第三个参数传入:

  1. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  2.  
  3. $this->generateUrl('blog_show', array('slug' => 'my-blog-post'), UrlGeneratorInterface::ABSOLUTE_URL);
  4. // http://www.example.com/blog/my-blog-post

Note

用于生成绝对链接的host主机名是通过 Request 对象自动侦测的。若要从web上下文之外来生成绝对URL (例如在命令行中),它不会工作。参考 如何在命令行中生成URL 以了解如何解决此问题。

除错

这里是一些你在使用路由时常见的报错信息:

Controller "AppBundleControllerBlogController::showAction()" requires that you provide a value for the "$slug" argument.

这个报错仅在你的控制器方法有一个参数时才会发生 (如 $slug):

  1. public function showAction($slug)
  2. {
  3. // ..
  4. }

但你的路由路径中 并没有 那个 {slug} 通配符 (如,它是 /blog/show)。可添加 {slug} 到路由路径中: /blog/show/{slug} 或者给予参数一个默认值 (即 $slug = null)。

Some mandatory parameters are missing ("slug") to generate a URL for route"blog_show".

这说明你正在为 blogshow 路由生成链接,但你 并没有_ 在路由的路径中传入一个 slug 值 (它是必须的,因为路由有 {slug}通配符)。解决办法是是,在生成路由(URL)时传入 slug 值:

  1. $this->generateUrl('blog_show', array('slug' => 'slug-value'));
  2.  
  3. // or, in Twig / 或者在Twig中
  4. // {{ path('blog_show', {'slug': 'slug-value'}) }}

路由的翻译

Symfony不支持基于语种以不同内容来定义路由。在那些场合下,你可以为每个控制器定义多个路由,每一个路由支持其所对应语言;或者使用社区的三方bundle来实现此功能,比如 JMSI18nRoutingBundleBeSimpleI18nRoutingBundle

总结

路由是一个将传入的请求之URL映射到用来处理该请求的控制器函数的系统。它允许你指定一个美观的URL,并使程序的功能性与URL“解耦”。路由是一个双向架构,意味着它也可以用来生成URL。

Keep Going!

路由,核对完毕!现在,去解封 控制器 的威力。

了解更多

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。