对于任何严谨的web应用程序而言漂亮的URL是绝对必须的。这意味着要离开诸如index.php?article_id=57这样丑陋的URL,而出现类似/read/intro-to-symfony的URL。

拥有灵活性是非常重要的。什么?你需要将页面的URL从/blog改为/news?你需要跟踪大量的链接以便在发生变化时更新它们?如果你使用Symfony2的路由,这将是容易的。

Symfony2路由让你定义有创意的URL去映射你应用程序不同的区域。在本章结束时,你将可以:

1、创建到控制器的复杂路由
2、在模板和控制器中生成URL
3、从Bundles中(也可以从其它什么地方)引导路由资源
4、调试你的路由

路由实战

一个是从URL模式到控制器的一个映射。举个例子,假设你想去匹配诸如/blog/my-post或/blog/all-about-symfony这样的URL,并将其发送到可以查找和渲染博文的控制器。路由是简单的:

# app/config/routing.yml
blog_show:
    pattern:   /blog/{slug}
    defaults:  { _controller: AcmeBlogBundle:Blog:show }

通过blog_show路由定义的模式对类似/blog/*的URL产生作用,而通配符被命名为slug。对于/blog/my-blog-post这样的URL,slug变量得到my-blog-post的值,并供你控制器使用。

_controller参数是一个特定的关键词,当URL匹配该路由时告诉控制器将被执行。_controller字符串被称为逻辑名,它遵循一个指向特定PHP类和方法的模式:

// src/Acme/BlogBundle/Controller/BlogController.php

namespace Acme/BlogBundle/Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function showAction($slug)
    {
        $blog = // 使用$blug变量去查询数据库

        return $this->render('AcmeBlogBundle:Blog:show.html.twig', array(
            'blog' => $blog,
        ));
    }
}

恭喜你!你已经创建了你的第一个路由,并将其与控制器连接。现在,当你访问/blog/my-post时,showAction控制器将被执行,其中$slug变量的值等于my-post。

Symfony2路由的目标:将请求的URL映射到控制器。遵循这一目标,你将学习到各式各样的技巧,甚至使映射大多数复杂的URL变得简单。

Routing: Under the Hood

当一个请求发给应用程序时,它包含了一个客户端要求的检索“资源”的地址。这一地址被称为URL(或URI),它可以是/contact、/blog/read-me或其它任何东西。下面是一个HTTP请求的例子:

GET /blog/my-blog-post

Symfony2路由系统的目标是解析URL,并确定执行哪个控制器。整个过程如下所示:

1、请求被Symfony2前端控制器(如:app.php)处理;
2、Symfony2的核心(如内核)要求路由检查该请求;
3、路由将输入URL匹配到特定路由条目,并返回该路由条目的信息,包括要执行的控制器;
4、Symfony2内核执行控制器,并最终返回Response对象。

 

Symfony2Book06:路由_Symfony2路由系统

路由是一个将输入URL转换成执行特定控制器的工具。

创建路由

Symfony为应用程序从单一的路由配置文件中引导所有的路由。该文件通常是app/config/routing.yml,但你也可以通过应用程序配置文件将该文件放置在任何地方(包括xml或php格式的配置文件)。

# app/config/config.yml
framework:
    # ...
    router:        { resource: "%kernel.root_dir%/config/routing.yml" }

尽管所有的路由都是从一个文件中引导,但常见的做法是在该文件内部包含其它路由资源。详情请参见包含外部路由资源。

基本路由配置

定义一条路由很方便,一个典型的应用程序有很多的路由。一条基本路由包括两个部分:匹配模式和defaults数组:

homepage:
    pattern:   /
    defaults:  { _controller: AcmeDemoBundle:Main:homepage }

该路由匹配首页(/)并将它映射到AcmeDemoBundle:Main:homepage控制器。_controller字符串被Symfony2转换成PHP函数并执行。这个过程在控制器命名模式中被简短提及。

带占位符的路由

当然路由系统支持更多有趣的路由,大多数路由将包含一个或多个被称为“通配符”的占位符:

blog_show:
    pattern:   /blog/{slug}
    defaults:  { _controller: AcmeBlogBundle:Blog:show }

模式将匹配任何类似/blog/*的URL,被{slug}占位符匹配的值可以用于控制器中。换句话说,如果URL是/blog/hello-world,$slug变量的值就是hello-world,将用于控制器中。这非常有用,比如说可以用它来引导相关的博文。

然而模式将不会匹配/blog,因为缺省状态下所有的占位符是必须匹配的。当然这也是可以变通的,可以在defaults数组中添加占位符的值来实现。

必须和可选的占位符

为了加点乐趣,我们添加一个新的路由为这个假想的博客应用程序显示所有可用的博文列表 :

blog:
    pattern:   /blog
    defaults:  { _controller: AcmeBlogBundle:Blog:index }

到目前为止,该路由是尽可能的简单,它没有包含通配符,也只是匹配/blog的URL。但如果你需要该路由支持分页,/blog/2显示列表的第2页呢?更新该路由,添加一个新的{page}占位符:

blog:
    pattern:   /blog/{page}
    defaults:  { _controller: AcmeBlogBundle:Blog:index }

就象以前的{slug}占位符一样,匹配{page}的值将在控制器中可用。它的值可以被用来确定在指定页显示的博文集。

但是就此打住!因为占位符缺省情况下是必须的,该路由将不再匹配简单的/blog。而要看第1页的博客,你必须要使用/blog/1这样的URL!因为没有办法让一个富web应用程序去操作、修改路由,让{page}参数可选。我们将通过包含中该路由中的defaults集来实现这一点:

blog:
    pattern:   /blog/{page}
    defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }

通过在defaults中添加page关键词,{page}占位符将不再被要求。/blog的URL将匹配该路由,并且page参数的值被设置为1。/blog/2的URL也将匹配给定的page参数:2。完美!

/blog {page} = 1
/blog/1 {page} = 1
/blog/2 {page} = 2

添加要求

现在看一看我们创建的路由:

blog:
    pattern:   /blog/{page}
    defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }

blog_show:
    pattern:   /blog/{slug}
    defaults:  { _controller: AcmeBlogBundle:Blog:show }

你能够看出问题吗?注意两个路由都匹配类似/blog/*的URL。Symfony2路由总是将选择它第一个匹配路由(blog),并返回一个my-blog-post的值给{page}参数。

URL 路由
 
参数
 
/blog/2 blog {page} = 2
/blog/my-blog-post blog {page} = my-blog-post

要解决这个问题需要添加路由要求。在本例中的路由中/blog/{page}仅当匹配{page}部分是整数时才工作。幸运的是可以很方便地为每个参数添加正则表达式要求。例如:

blog:
    pattern:   /blog/{page}
    defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }
    requirements:
        page:  \d+

\d+要求是一个正则表达式,意思是{page}参数的值必须是数字。blog路由将匹配类似/blog/2这样的URL(因为2是数字),但它不再匹配类似/blog/my-blog-post这样的URL(因为my-blog-post不是数字)。

URL 路由 参数
/blog/2 blog {page} = 2
/blog/my-blog-post blog_show {slug} = my-blog-post

更早的路由总是赢

这句话的意思就是路由的顺序是十分重要的。如果blog_show路由在blog路由前面的话,那么/blog/2这样的URL将匹配blog_show将代替blog,因为blog_show中的{slug}参数没有要求。通过适当的顺序和巧妙的要求,你可以完成任何事情。

因为参数要求是正则表达式,每个要求的复杂程度和灵活性都完全由你控制。假定你应用程序的首页基于URL可以在两个不同语言中使用:

homepage:
    pattern:   /{culture}
    defaults:  { _controller: AcmeDemoBundle:Main:homepage, culture: en }
    requirements:
        culture:  en|fr

根据传入请求,URL的{culture}部分将匹配正规表达式(en|fr)

For incoming requests, the {culture} portion of the URL is matched against the regular expression (en|fr) .

/ {culture} = en
/en {culture} = en
/fr {culture} = fr
/es 不匹配本路由

添加对HTTP方法的要求

除了URL,你也可以匹配传入请求的方法(如GET、HEAD、POST、PUT和DELET)。假设你一个联系人表单有两个控制器,一个显示表单(GET请求)一个处理提交的表单(POST请求),那么它可以通过以下路由配置来实现:

contact:
    pattern:  /contact
    defaults: { _controller: AcmeDemoBundle:Main:contact }
    requirements:
        _method:  GET

contact_process:
    pattern:  /contact
    defaults: { _controller: AcmeDemoBundle:Main:contactProcess }
    requirements:
        _method:  POST

尽管这两个路由有着相同的模式(/contact),但第1个路由将只匹配GET请求,而第2个路由将只匹配POST请求。这就意味着你可以通过同样的URL去显示和提交表单,而为这两个动作调用不同的控制器。

如果没有指定_mothod要求,路由将匹配所有的方法。

象其它要求一样,_method要求被当作一个正则表达式来分析,为了匹配GET或POST请求,你可以使用GET|POST。

高级路由示例

在Symfony2中你可以通过创建一个强大的路由结构来实现你所需的一切。下面是一个示例来展示路由系统是如何的灵活:

article_show:
  pattern:  /articles/{culture}/{year}/{title}.{_format}
  defaults: { _controller: AcmeDemoBundle:Article:show, _format: html }
  requirements:
      culture:  en|fr
      _format:  html|rss
      year:     \d+

正如你所看到的那样,这个路由仅在URL的{culture}部分是en或fr、{year}部分是数字的情况下才会被匹配。该路由还向你展示了你可以使用一个句号来分割两个占位符。URL匹配如下路由:

/articles/en/2010/my-post
/articles/fr/2010/my-post.rss

特殊的_format路由参数

这个示例也突显了特殊的_format路由参数。当使用这个参数时,匹配值将成为Request对象的请求格式。最终,请求格式被用于象响应的Content-Type这样的设置(如一个json请求格式将转换成application/json的Content-Type)。它也可以在控制器中使用,根据不同的_format值去渲染不同的模板。_format参数是非常强大的。它可以用不同的格式去渲染同一内容。

控制器命名模式

每个路由都必须有一个_controller参数,以便当路由被匹配时去确定哪个控制器执行。这个参数使用一个简单的字符串模式叫控制器逻辑名,Symfony2将映射一个指定的PHP方法或类。这个模式有三个部分并用冒号隔开:

bundle:controller:action

例如,_controller的值是AcmeBlogBundle:BlogController:showAction,那么:

Bundle 控制器类 方法名
 
AcmeBlogBundle BlogController showAction

这个控制器如下所示:

// src/Acme/BlogBundle/Controller/BlogController.php

namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function showAction($slug)
    {
        // ...
    }
}

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

你也可以使用它的全格式类名和方法来指定这个类:Acme\BlogBundle\Controller\BlogController::showAction。但如果你进行一些简单的转换,逻辑名将更加简洁也更加灵活。

除了使用逻辑名和全格式类名之外,Symfony2也支持第三种指定控制器的方式。这种方式只使用一个冒号分隔(如service_name:indexAction)并将控制器设为一个服务(参见如何将控制器定义成服务)。

路由参数和控制器参数

路由参数(如{slug}是非常重要的,因为它(们)都被用作控制器方法的参数:

public function showAction($slug)
{
  // ...
}

实际上,defaults集将参数值一起合并成一个表单数组。数组中的每个关键词都被做为控制器参数。

换句话说,对于控制器方法的每个参数,Symfony2都会根据名称来查找路由参数,并将其值指向控制器参数。在上面的高级示例当中,下列变量的任何组合(以任意方式)都被用作showAction()方法的参数:

$culture
$year
$title
$_format
$_controller

占位符和defaults集被合并在一起,就是$_controller变量也是可用的。更多细节的讨论,请参见作为控制器参数的路由参数。

你也可以使用指定的$_route变量,它的值是被匹配的路由名。

包含外部路由资源

所有的路由都通过单个配置文件引导,通常是app/config/routing.yml(参见上面的创建路由)。然而,通常你想从其它地方引导路由,象一个在bundle中的路由文件,可以通过“导入”该文件来实现:

# app/config/routing.yml
acme_hello:
    resource: "@AcmeHelloBundle/Resources/config/routing.yml"

当从YAML导入资源时,关键词(如acme_hello)是没有意义的。仅仅只要确保它是唯一的,没有其它行覆盖它即可。

resource关键词引导给定的路由资源。在这个例子中资源是全路径的文件,@AcmeHelloBundle是到该Bundle路径的快捷方式。被导入的文件如下所示:

 # src/Acme/HelloBundle/Resources/config/routing.yml
acme_hello:
     pattern:  /hello/{name}
     defaults: { _controller: AcmeHelloBundle:Hello:index }

来自这个文件的路由被解析并以主路由文件相同的方式引导。

使用前缀来导入路由

你也可以选择为导入的路由提供一个“前缀”。举个例子,假设你想acme_hello路由最终用/admin/hello/{name}来代替简单的/hello/{name}:

# app/config/routing.yml
acme_hello:
    resource: "@AcmeHelloBundle/Resources/config/routing.yml"
    prefix:   /admin

字符串/admin现在将前置在新路由资源中引导的每条路由模式的前面。

可视化和调试路由

当添加和自定义路由时,能够可视化和获得路由的详细信息是非常有用的。查看应用程序中每条路由的最好方法是使用命令行router:debug。这可以在你项目的根目录中运行以下命令实现:

php app/console router:debug

该命令将打印应用程序中所有配置路由的列表,这十分有用:

homepage              ANY       /
contact               GET       /contact
contact_process       POST      /contact
article_show          ANY       /articles/{culture}/{year}/{title}.{_format}
blog                  ANY       /blog/{page}
blog_show             ANY       /blog/{slug}

你也可以通过在命令后包含路由名来获取该路由上的非常特殊的信息。

php app/console router:debug article_show

生成URL

路由系统也可以用来生成URL。其实,路由是一个双向系统:映射URL到控制器+参数以及映射路由+参数回URL。match()和generate()方法构成了这个双向系统。使用先前blog_show的例子:

$params = $router->match('/blog/my-blog-post');
// array('slug' => 'my-blog-post', '_controller' => 'AcmeBlogBundle:Blog:show')

$uri = $router->generate('blog_show', array('slug' => 'my-blog-post'));
// /blog/my-blog-post

要生成一个URL,你需要指定用于路由的路由名(如:blog_show)和模式通配符(如:slug=my-blog-post)。有了这些信息,任何URL都可以很容易地生成:

class MainController extends Controller
{
    public function showAction($slug)
    {
      // ...

      $url = $this->get('router')->generate('blog_show', array('slug' => 'my-blog-post'));
    }
}

接下来,你将学习到如何从内部模板中生成URL。

生成绝对URL

缺省情况下,路由将生成相对URL(如:/blog)。要生成绝对URL,只需要在generate()方法中将第三个参数设为true即可:

$router->generate('blog_show', array('slug' => 'my-blog-post'), true);
// http://www.example.com/blog/my-blog-post

当生成绝对URL时所使用的主机是当前Request对象的主机名。它是通过基于支持PHP的服务器信息自动匹配的。当通过命令行脚本生成绝对URL时,你必须手工设置Request对象的所需主机:

$request->headers->set('HOST', 'www.example.com');

生成带问号字符串的URL

generate方法可以使用通配符数组的值去生成URI。但如果你要添加一些附加内容,它将作为带问号的字符串添加在URI后面:

$router->generate('blog', array('page' => 2, 'category' => 'Symfony'));
// /blog/2?category=Symfony

从模板中生成URL
 

最常见的生成URL的地方是当应用程序连接两个页面时从模板内部生成的。下面示例与前面一样,只是使用了模板帮手函数:

<a href="{{ path('blog_show', { 'slug': 'my-blog-post' }) }}">
  <!--读篇该博文-->
</a>

也可以生成绝对URL

<a href="{{ url('blog_show', { 'slug': 'my-blog-post' }), true }}">
  <!--读篇该博文-->
</a>

小结

路由是一个将传入请求URL映射到用来处理请求的控制器函数的系统。它允许你指定一个漂亮的URL,并使应用程序的功能与URL“脱钩”。路由是一个双向的机制,意味着它也可以用来生成URL。