对于任何严谨的web应用程序而言漂亮的URL是绝对必须的。这意味着诸如index.php?article_id=57这样丑陋的URL,要被这样的/read/intro-to-symfony的URL所替代。
拥有灵活性是非常重要的。什么?你需要将页面的URL从/blog改为/news?你需要跟踪大量的链接以便在发生变化时更新它们?如果你使用Symfony2的路由,你根本不用担心因为这很容易。
Symfony路由器允许您定义创造性的url,映射到应用程序的不同区域。在本章结束时,你将可以做到:
1.创建复杂的路由到控制器
2.在模板和控制器中生成URL
3.从Bundles中(也可以从其它什么地方)引导路由资源
4.调试你的路由
路由实战
一个路由是一个URL路径到一个控制器的映射。例如,你想匹配一些URL:/blog/my-post 和 /blog/all-about-symfony并且发送到一个能够查询和渲染博文的控制器上。路由只需简单的设置:
Annotations方式
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 Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class BlogController extends Controller { /** * @Route("/blog/{slug}") */ public function showAction($slug) { // ... } } |
YAML方式:
1 2 3 4 |
# app/config/routing.yml blog_show: path: /blog/{slug} defaults: { _controller: AppBundle:Blog:show } |
定义blog_show路由模式,用于匹配像/blog/*的URL,把相关参数或通配符用slug表示并传入。对于/blog/my-blog-post这样的URL,slug变量得到my-blog-post的值,并供你控制器使用。这个blog_show是一个内部名称,他没什么实际的意义就是一个唯一的标识。以后,你可用他来生成一些URL。
如果你不想去使用annotations,因为你不喜欢他们,或者因为你不希望依赖于SensioFrameworkExtraBundle,你也可以使用YAML,XML或者PHP。在这些格式中,_controller参数是一个特殊的键,它告诉symfony路由指定的URL应该执行哪个控制器。_controller字符串称为逻辑名。它遵循规则指向一个特定的php类和方法,AppBundle\Controller\BlogController::showAction方法。
恭喜!您刚刚创建了一个路由并把它连接到控制器。现在,当你访问/blog/my-post,showAction控制器将被执行并且$slug变量就等于my-post。
Symfony2路由的目标:将请求的URL映射到控制器。遵循这一目标,你将学习到各式各样的技巧,甚至使映射大多数复杂的URL变得简单。
路由:深入了解
当一个请求发送到您的应用程序,它包含一个确切的“资源”的客户端请求地址。该地址被称为URL(或URI),它可以是/contact、/blog/read-me或其它任何东西。下面是一个HTTP请求的例子:
1 |
GET /blog/my-blog-post |
symfony路由系统的目的是解析url,并确定调用哪个控制器。整个过程是这样的:
1. 由Symfony的前端控制器(app.php)来处理请求。
2.symfony的核心(Kernel内核)要求路由器来检查请求。
3.路由将输入的URL匹配到一个特定的路由,并返回路由信息,其中包括要执行的控制器信息。
4.Symfony内核执行控制器并最终返回Response对象。
创建路由
Symfony从一个单一的路由配置文件中加载所有的路由到你的应用程序。这个路由配置文件通常是app/config/routing.yml,但你也可以通过应用程序配置文件将该文件放置在任何地方(包括xml或php格式的配置文件)。
1 2 3 4 |
# app/config/config.yml framework: # ... router: { resource: "%kernel.root_dir%/config/routing.yml" } |
尽管所有的路由都可以从一个文件加载,但是通常的做法是包含额外的路由资源。为此,你要把外部的路由文件配置到主要路由文件中。具体信息可查看本章:包含外部路由资源。
基本的路由配置
定义一个路由是容易的,一个典型的应用程序也应该有很多的路由。一个基本的路由包含两个部分:路由匹配和默认数组:
Annotations方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// src/AppBundle/Controller/MainController.php // ... class MainController extends Controller { /** * @Route("/") */ public function homepageAction() { // ... } } |
该路由匹配首页(/)并将它映射到AcmeDemoBundle:Main:homepage控制器。_controller字符串被Symfony2转换成PHP函数去执行。这个过程在本章(控制器命名模式)中被简短提及。
带参数的路由
路由系统支持很多有趣的路由写法。许多的路由都可以包含一个或者多个“参数或通配符”占位符:
Annotations方式
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// src/AppBundle/Controller/BlogController.php // ... class BlogController extends Controller { /** * @Route("/blog/{slug}") */ public function showAction($slug) { // ... } } |
这个路径将匹配任何/blog/*的URL。更妙的是,这个{slug}占位符将自动匹配到控制器中。换句话说,如果该URL是/blog/hello-world,控制器中$slug变量的值就是hello-world。这可以用来,匹配博客文章标题的字符串。
然而这种方式路由将不会匹配/blog这样的URL,因为默认情况下,所有的占位符都是必填的。当然这也是可以变通的,可以在defaults数组中添加占位符(参数)的值来实现。
必填和选填的参数(占位符)
为了让教程更加精彩,我们添加一个新的路由为这个假想的博客应用程序显示所有可用的博文列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/AppBundle/Controller/BlogController.php // ... class BlogController extends Controller { // ... /** * @Route("/blog") */ public function indexAction() { // ... } } |
到目前为止,该路由是尽可能的简单,它没有包含通配符(参数),也只是匹配/blog的URL。但如果你需要该路由支持分页,/blog/2显示列表的第2页呢?更新该路由,添加一个新的{page}占位符:
1 2 3 4 5 6 7 8 9 10 11 |
// src/AppBundle/Controller/BlogController.php // ... /** * @Route("/blog/{page}") */ public function indexAction($page) { // ... } |
跟之前的{slug}占位符一样,这个{page}值也会匹配到你的控制器中。这个值主要是用来给博客系统分页的。
但是有问题!占位符缺省情况下是必须的,该路由将不再匹配简单的/blog。而要看第1页的博客,你必须要使用/blog/1这样的URL!也没有一个办法让富web应用程序去操作修改路由或者让{page}参数可选。这时symfony的路由提供了一个defaults集来配置默认值,实现这些操作:
1 2 3 4 5 6 7 8 9 10 11 |
// src/AppBundle/Controller/BlogController.php // ... /** * @Route("/blog/{page}", defaults={"page" = 1}) */ public function indexAction($page) { // ... } |
通过添加page的defaults值,{page}占位符在URL中可以选填了。当URL输入/blog时路由匹配/blog/1,page参数这时默认为1。/blog/2的URL传入后,page参数为2。完美了!
URL | Route | Parameters |
---|---|---|
/blog |
blog |
{page} = 1 |
/blog/1 |
blog |
{page} = 1 |
/blog/2 |
blog |
{page} = 2 |
当然你还可以有多个可选的占位符(如/blog/{slug}/{page}),但是如果第一个占位符是可选的,后台一切的占位符都必须是可选的。举例,
/{page}/blog 是一个有效的路径,但是page必须是必填的(如果不是必填的那么路由会匹配之前简单的/blog,而不是匹配/{page}/blog)。
这些路由在选填参数时,不会匹配带斜杠的请求(如/blog/不匹配,/blog匹配正确)。
添加要求
快速浏览一下已经创建的路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/AppBundle/Controller/BlogController.php // ... class BlogController extends Controller { /** * @Route("/blog/{page}", defaults={"page" = 1}) */ public function indexAction($page) { // ... } /** * @Route("/blog/{slug}") */ public function showAction($slug) { // ... } } |
你能发现问题吗?两个路由都匹配类似/blog/*的URL。Symfony2路由总是选择它第一个匹配的(blog)路由。换句话说,该blog_show路由永远被匹配。相反,像一个/blog/my-blog-post的URL会匹配第一个(blog)路由,并返回一个my-blog-post的值给{page}参数。
URL | Route | Parameters |
---|---|---|
/blog/2 |
blog |
{page} = 2 |
/blog/my-blog-post |
blog |
{page} = "my-blog-post" |
这个问题的答案是增加路由的要求或者路由的条件(可查看本章完全自定义路由匹配条件)。在本例中如何让路由/blog/{page}仅匹配{page}部分是整数时才工作。幸运的是symfony可以很方便地为每个参数添加正则表达式要求。例如:
1 2 3 4 5 6 7 8 9 10 11 |
// src/AppBundle/Controller/BlogController.php // ... /** * @Route("/blog/{page}", defaults={"page": 1}, requirements={"page": "\d+"}) */ public function indexAction($page) { // ... } |
这个\d+是一个正则表达式,意思是{page}参数的值必须是数字。blog的路由将匹配像/blog/2这样的URL(因为2是一个数字),但它不再匹配类似/blog/my-blog-post这样的URL(因为my-blog-post不是数字)。
URL | Route | Parameters |
---|---|---|
/blog/2 |
blog |
{page} = 2 |
/blog/my-blog-post |
blog_show |
{slug} = my-blog-post |
/blog/2-my-blog-post |
blog_show |
{slug} = 2-my-blog-post |
早些时候的路由总能赢
这一切意味着路由的顺序是非常重要的。如果blog_show路由在Blog博客前面的话,那么/blog/2这样的URL将匹配blog_show,而不是blog,因为blog_show中的{slug}参数没有要求。通过适当的顺序和巧妙的要求,你可以完成任何事情。
由于参数要求是正则表达式,每个要求的复杂程度和灵活性都完全由你控制。假定你应用程序的网页URL基于两个不同语言中使用:
Annotations方式
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/AppBundle/Controller/MainController.php // ... class MainController extends Controller { /** * @Route("/{_locale}", defaults={"_locale": "en"}, requirements={"_locale": "en|fr"}) */ public function homepageAction($_locale) { } } |
根据传入的请求,{_locale}的URL部分要匹配正则表达式(en|fr)。
Path | Parameters |
---|---|
/ |
{_locale} = "en" |
/en |
{_locale} = "en" |
/fr |
{_locale} = "fr" |
/es |
won’t match this route |
添加HTTP方法的要求
除了URL,你也可以把匹配的方法传入请求(如GET、HEAD、POST、PUT、DELETE)。假设你一个联系人表单有两个控制器,一个显示表单(GET请求)一个处理提交的表单(POST请求),那么它可以通过以下路由配置来实现:
Annotations方式
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 |
// src/AppBundle/Controller/MainController.php namespace AppBundle\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; // ... class MainController extends Controller { /** * @Route("/contact") * @Method("GET") */ public function contactAction() { // ... display contact form } /** * @Route("/contact") * @Method("POST") */ public function processContactAction() { // ... process contact form } } |
尽管这两个路由有着相同的路径(/contact),但第1个路由将只匹配GET请求,而第2个路由将只匹配POST请求。这就意味着你可以通过同样的URL去显示和提交表单,而为这两个动作调用不同的控制器。
如果没有methods被指定,这个路由将匹配所有方法。
添加主机要求
你也可以匹配HTTP上的主机传入的请求。欲了解更多请求,请参阅 How to Match a Route Based on the Host 。
完全自定义路由匹配条件
2.4 路由条件在symfony2.4被引入
正如你所看到的,路由可以只匹配特定的路由通配符(通过正则表达式),HTTP方法和主机名。但是路由系统的conditions:可以扩展到一个几乎无限灵活性的地步。
1 2 3 4 |
contact: path: /contact defaults: { _controller: AcmeDemoBundle:Main:contact } condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" |
condition是一个表达式,你可以了解更多关于该表达式的语法。有了这个,该路由就不匹配了,除非HTTP方法是GET和HEAD,又或者是User-Agent头是firefox。
你可以利用传递到表达式的两个变量做任何你需要的复杂逻辑:
context:
一个RequestContext实例化,包含路由开始匹配的大部分基本信息。
request:
这个是symfony2的Request对象。
当生成一个URL时,不用考虑Conditions。
表达式被编译到php
在幕后表达式被编译成原始的php,在我们的例子中会生成php到缓存目录下:
123456 if (rtrim($pathinfo, '/contact') === '' && (in_array($context->getMethod(), array(0 => "GET", 1 => "HEAD"))&& preg_match("/firefox/i", $request->headers->get("User-Agent")))) {// ...}正因为如此,condition键使用底层php执行,
没有产生额外的开销。
高级的路由样例
在Symfony2中你可以通过创建一个强大的路由结构来实现你所需的一切。下面是一个示例来展示路由系统是如何的灵活:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/AppBundle/Controller/ArticleController.php // ... class ArticleController extends Controller { /** * @Route( * "/articles/{_locale}/{year}/{title}.{_format}", * defaults: {"_format": "html"} * requirements: {"_locale": "en|fr", "_format": "html|rss", "year": "\d+"} * ) */ public function showAction($_locale, $year, $title) { } } |
正如你所看到的,这个路由只匹配一部分URL也就是满足{_locale}为(en或者
fr)和
{year}是数字的。该路由还向你展示了你可以使用一个句号来分割两个占位符。上面路由匹配的URL如下:
/articles/en/2010/my-post
/articles/fr/2010/my-post.rss
/articles/en/2013/my-latest-post.html
这个特殊的_format路由参数
这个示例也突显了特殊的_format路由参数。当使用这个参数时,匹配值将成为Request对象的请求格式。最终,请求格式被用于响应的Content-Type这样的设置(如一个json请求格式将转换成application/json的Content-Type)。它也可以在控制器中使用,根据不同的_format值去渲染不同的模板。_format参数是非常强大的。它可以用不同的格式去渲染同一内容。
有些时候你希望路由配置的某些部分在全局使用。symfony可以利用服务容器参数来做到这一点。了解更多,How to Use Service Container Parameters in your Routes。
特殊的路由参数
正如你所看到的,每个路由参数或默认值都可以作为控制器方法的参数。此外,有三个参数是特殊的:在你的应用程序中每增加一个独特的功能:
_controller
正如你所看到的,这个参数是用来匹配执行控制器的。
_format
用于设置request格式。
_locale
用于设置语言环境的(阅读更多)。
控制器命名模式
每个路由都必须有一个_controller参数,以便当路由被匹配时去确定哪个控制器执行。这个参数使用一个简单的字符串模式叫控制器逻辑名,Symfony2将映射一个指定的PHP方法或类。这个模式有三个部分并用冒号隔开:
1 |
bundle:controller:action |
假设,一个_controller值是一个AppBundle:Blog:show那么意味着:
Bundle | Controller Class | Method Name |
---|---|---|
AppBundle | BlogController |
showAction |
该控制器可能是这样的:
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) { // ... } } |
注意,Symfony2在Blog上添加了字符串Controller作为类名(Blog=>BlogController),添加字符串Action作为方法名(show=>showAction)。
你也可以使用它的全格式类名和方法来指定这个类:Acme\BlogBundle\Controller\BlogController::showAction。但如果你进行一些简单的转换,逻辑名将更加简洁也更加灵活。
除了使用逻辑名和全格式类名之外,Symfony2也支持第三种指定控制器的方式。这种方式只使用一个冒号分隔(如service_name:indexAction)并将控制器设为一个服务(参见如何将控制器定义成服务)。
路由参数和控制器参数
路由参数(如{slug}是非常重要的,因为它(们)都被用作控制器方法的参数:
1 2 3 4 |
public function showAction($slug) { // ... } |
现实中,defaults集将参数值一起合并成一个表单数组。该数组中的每个键都被做为控制器的参数。
换句话说,对于控制器方法的每个参数,Symfony2都会根据该名称来查找路由参数,并将其值指向到控制器作为参数。在上面的高级示例当中,下列变量的任何组合(以任意方式)都被用作showAction()方法的参数:
$_locale
$year
$title
$_format
$_controller
$_route
占位符和defaults集被合并在一起,就就算是$_controller变量也是可用的。更多细节的讨论,请参见作为 第五章:控制器–把路由参数传入控制器。
你也可以使用指定的$_route变量,它的值是被匹配的路由名。
你甚至可以在你的路由中定义额外的信息并在你的控制器中访问它。关于更多信息请阅读 How to Pass Extra Information from a Route to a Controller
包含外部路由资源
所有的路由都通过一个单一的配置文件加载-通常是app/config/routing.yml(请看创建路由)。但是,如果你使用路由的annotations方式,你需要路由指向到带有annotations的控制器。这种方式你可以通过“importing”来引入路由配置目录:
1 2 3 4 |
# app/config/routing.yml app: resource: "@AppBundle/Controller/" type: annotation # required to enable the Annotation reader for this resource |
当从YAML导入资源时,关键词(如acme_hello)是没有意义的。仅仅只要确保它是唯一的,没有其它行覆盖它即可。
这个resource键加载指定的路由资源。在这个例子中资源是一个目录,在这里@AppBundle能够解析AppBundle的完整路径。当路由指定一个目录,该目录中的所有文件都会被解析到该路由中去。
你还可以包含其他路由配置文件,这是经常被用来导入第三方路由的方法:
1 2 3 |
# app/config/routing.yml app: resource: "@AcmeOtherBundle/Resources/config/routing.yml" |
在引入的路由中加前缀
你也可以选择为导入的路由提供一个“prefix”前缀。例如,你想在AppBundle的所有路由中使用前缀/site( 用/site/blog/{slug}代替
/blog/{slug}
):
1 2 3 4 5 |
# app/config/routing.yml app: resource: "@AppBundle/Controller/" type: annotation prefix: /site |
这个路径下的每个路由资源都加载了新的前缀字符串/site.
添加一个主机要求并引入到路由
你可以在引入路由配置主机的正则表达式。更多细节请查看 Using Host Matching of Imported Routes
可视化和调试路由
当添加和自定义路由时,能够可视化的让你获得路由的详细信息是非常有用的。有一个伟大的方式,就是查看应用程序中每条路由的最好方法是使用命令行debug:router。可以在你项目的根目录中运行以下命令实现:
1 |
$ php app/console debug:router |
在symfony2.6之前该命令被称为为
router:debug
.
该命令将打印应用程序中所有配置路由的列表,这十分有用:
1 2 3 4 5 6 |
homepage ANY / contact GET /contact contact_process POST /contact article_show ANY /articles/{_locale}/{year}/{title}.{_format} blog ANY /blog/{page} blog_show ANY /blog/{slug} |
你只需要在这个命令上加入一个的路由名称,就可以查看你指定的路由具体信息。
1 |
$ php app/console debug:router article_show |
同样,如果你想测试一个URL是否能够匹配一个指定的路由,你可以使用router:match命令:
1 |
$ php app/console router:match /blog/my-latest-post |
此命令将打印出URL匹配的路由
1 |
Route "blog_show" matches |
生成URL
路由系统也用于生成URL。在现实中,路由是一个双向系统:映射URL到控制器+参数以及映射路由+参数返回URL。match()和generate()方法构成了这个双向系统。使用之前的blog_show的例子:
1 2 3 4 5 6 7 8 |
$params = $this->get('router')->match('/blog/my-blog-post'); // array( // 'slug' => 'my-blog-post', // '_controller' => 'AppBundle:Blog:show', // ) $uri = $this->get('router')->generate('blog_show', array('slug' => 'my-blog-post')); // /blog/my-blog-post |
要生成一个URL,你需要指定路由的名称(如blog_show)以及任意的通配符(如slug = my-blog-post)。有个这些信息,任何URL就可以很容易的生成了:
1 2 3 4 5 6 7 8 9 10 11 12 |
class MainController extends Controller { public function showAction($slug) { // ... $url = $this->generateUrl( 'blog_show', array('slug' => 'my-blog-post') ); } } |
在控制器中你没有继承symfony2的父类Controller,你可以使用router的generate()服务方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use Symfony\Component\DependencyInjection\ContainerAware; class MainController extends ContainerAware { public function showAction($slug) { // ... $url = $this->container->get('router')->generate( 'blog_show', array('slug' => 'my-blog-post') ); } } |
在即将到来的部分中,你将学会如何在模板中生成URL地址。
如果您的应用程序前端使用的是ajax请求,你可能希望根据你的路由配置,在JavaScript中生成URL。通过使用FOSJsRoutingBundle,你就可以做到:
1234 var url = Routing.generate('blog_show',{"slug": 'my-blog-post'});更多信息请阅读这个bundle文档。
生成的URL带有Query Strings(?xxx=xxxxxx)
这个generate方法采用通配符数组来生成URL。但是如果在其中添加了额外的键值对,他们将会被添加成Query Strings来生成一个新的URL:
1 2 |
$this->get('router')->generate('blog', array('page' => 2, 'category' => 'Symfony')); // /blog/2?category=Symfony |
在模板里生成URL
在应用程序页面之间进行连接时,最常见的地方就是从模板中生成URL。这样做其实和以前一样,但是使用的是一个模板助手函数:
1 2 3 |
<a href="{{ path('blog_show', {'slug': 'my-blog-post'}) }}"> Read this blog post. </a> |
生成绝对的URL
默认情况下,路由器会产生相对的URL(如/blog)。在控制器中,很简单的把generateUrl()方法的第三参数设置成true即可。
1 2 |
$this->generateUrl('blog_show', array('slug' => 'my-blog-post'), true); // http://www.example.com/blog/my-blog-post |
在模板引擎twig中,要使用url()函数(生成一个绝对的URL),而不是path()函数(生成一个相对的URL)。在php中,需要要在generateUrl()中传入true:
1 2 3 |
<a href="{{ url('blog_show', {'slug': 'my-blog-post'}) }}"> Read this blog post. </a> |
当生成一个绝对URL链接时,所使用的主机自动检测当前使用的Request对象。当生成从web环境外的绝对URL(例如一个控制台命令)这是行不通的。请参见How to Generate URLs and Send Emails from the Console来学习如何解决这个问题。
摘要
路由是一个将传入请求URL映射到用来处理请求的控制器函数的系统。它允许你指定一个漂亮的URL,并使应用程序的功能与URL“脱钩”。路由是一个双向的机制,意味着它也可以用来生成URL。
了解更多可以查看 cookbook
怎么没有九,十,十一?
这里的文档如果不能满足你,你可以来到我们的 http://www.symfonychina.com 看看是否有你需要的。