路由...修复中

3.3 version
维护中的版本

对于任何严谨的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,并且把路由发送到一个“能够查询和输出该篇博文”的控制器。这个路由很简单:

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
27
28
29
30
31
32
33
// 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'
        // $slug 必须等同于URL中的动态部分
        // 即,在 /blog/yay-routing 中 $slug='yay-routing'
 
        // ...
    }
}
1
2
3
4
5
6
7
8
# app/config/routing.yml
blog_list:
    path:     /blog
    defaults: { _controller: AppBundle:Blog:list }

blog_show:
    path:     /blog/{slug}
    defaults: { _controller: AppBundle:Blog:show }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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;

正在修复中: 阅读时请注意,本文档以下部分正待修复。我们将尽快完成校对。

定义blog_show路由模式,用于匹配像/blog/*的URL,把相关参数或通配符用slug表示并传入。对于/blog/my-blog-post这样的URL,slug变量得到my-blog-post的值,并供你控制器使用。美观blog_show是一个内部名称,他没什么实际的意义就是一个唯一的标识。以后,你可用他来生成一些URL。

如果你不想去使用注释方式,因为你不喜欢他们,或者因为你不希望依赖于SensioFrameworkExtraBundle,你也可以使用YAML,XML或者PHP。在这些格式中,_controller参数是一个特殊的键,它告诉symfony路由指定的URL应该执行哪个控制器。_controller字符串称为逻辑名。它遵循规则指向一个特定的php类和方法,AppBundle\Controller\BlogController::showAction方法。

恭喜!你刚刚创建了一个路由并把它连接到控制器。现在,当你访问/blog/my-postshowAction控制器将被执行并且$slug变量就等于my-post

Symfony路由的目标:将请求的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对象。

request-flow

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

创建路由 

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

1
2
3
4
# app/config/config.yml
framework:
    # ...
    router: { resource: '%kernel.root_dir%/config/routing.yml' }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
 
    <framework:config>
        <!-- ... -->
        <framework:router resource="%kernel.root_dir%/config/routing.xml" />
    </framework:config>
</container>
1
2
3
4
5
6
7
// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'router' => array(
        'resource' => '%kernel.root_dir%/config/routing.php',
    ),
));

Tip

尽管所有的路由都可以从一个文件加载,但是通常的做法是包含额外的路由资源。为此,你要把外部的路由文件配置到主要路由文件中。具体信息可查看本章:包含外部路由资源

基本的路由配置 

定义一个路由是容易的,一个典型的应用程序也应该有很多的路由。一个基本的路由包含两个部分:path 匹配和defaults数组:

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()
    {
        // ...
    }
}
1
2
3
4
# app/config/routing.yml
_welcome:
    path:      /
    defaults:  { _controller: AppBundle:Main:homepage }
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 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="_welcome" path="/">
        <default key="_controller">AppBundle:Main:homepage</default>
    </route>
 
</routes>
1
2
3
4
5
6
7
8
9
10
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
 
$collection = new RouteCollection();
$collection->add('_welcome', new Route('/', array(
    '_controller' => 'AppBundle:Main:homepage',
)));
 
return $collection;

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

带参数的路由 

路由系统支持很多有趣的路由写法。许多的路由都可以包含一个或者多个“参数或通配符”占位符:

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)
    {
        // ...
    }
}
1
2
3
4
# app/config/routing.yml
blog_show:
    path:      /blog/{slug}
    defaults:  { _controller: AppBundle:Blog:show }
1
2
3
4
5
6
7
8
9
10
11
<!-- 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_show" path="/blog/{slug}">
        <default key="_controller">AppBundle:Blog:show</default>
    </route>
</routes>
1
2
3
4
5
6
7
8
9
10
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
 
$collection = new RouteCollection();
$collection->add('blog_show', new Route('/blog/{slug}', array(
    '_controller' => 'AppBundle:Blog:show',
)));
 
return $collection;

美观路径将匹配任何/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
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)
    {
        // ...
    }
}
1
2
3
4
5
6
7
8
# app/config/routing.yml
blog:
    path:      /blog/{page}
    defaults:  { _controller: AppBundle:Blog:index, page: 1 }

blog_show:
    path:      /blog/{slug}
    defaults:  { _controller: AppBundle:Blog:show }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 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" path="/blog/{page}">
        <default key="_controller">AppBundle:Blog:index</default>
        <default key="page">1</default>
    </route>
 
    <route id="blog_show" path="/blog/{slug}">
        <default key="_controller">AppBundle:Blog:show</default>
    </route>
</routes>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
 
$collection = new RouteCollection();
$collection->add('blog', new Route('/blog/{page}', array(
    '_controller' => 'AppBundle:Blog:index',
    'page'        => 1,
)));
 
$collection->add('blog_show', new Route('/blog/{show}', array(
    '_controller' => 'AppBundle:Blog:show',
)));
 
return $collection;

你能发现问题吗?两个路由都匹配类似/blog/*的URL。Symfony路由总是选择它第一个匹配的(blog)路由。换句话说,该blog_show路由永远被匹配。相反,像一个/blog/my-blog-post的URL会匹配第一个(blog)路由,并返回一个my-blog-post的值给{page}参数。

URL Route Parameters
/blog/2 blog_list $page = 2
/blog/my-blog-post blog_show $slug = my-blog-post

给{通配符}一个默认值 

高级的路由样例 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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)
    {
    }
}
1
2
3
4
5
6
7
8
# app/config/routing.yml
article_show:
  path:     /articles/{_locale}/{year}/{title}.{_format}
  defaults: { _controller: AppBundle:Article:show, _format: html }
  requirements:
      _locale:  en|fr
      _format:  html|rss
      year:     \d+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 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}/{title}.{_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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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}/{title}.{_format}', array(
        '_controller' => 'AppBundle:Article:show',
        '_format'     => 'html',
    ), array(
        '_locale' => 'en|fr',
        '_format' => 'html|rss',
        'year'    => '\d+',
    ))
);
 
return $collection;

正如你所看到的,美观路由只匹配一部分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对象的“request format”(请求格式)。

最终,请求格式被用在“设置响应的Content-Type”这种地方(如一个json请求格式将转换成application/jsonContent-Type)。它也可以在控制器中使用,根据不同的_format值去渲染不同的模板。_format参数是一种非常强大的方式,把相同的内容以不同格式来渲染(译注:即输出)。

在symfony3.0之前的版本中,可以覆写request(对象)中的格式参数(_format),通过添加名为“_format”的query参数即可(例:/foo/bar?_format=json)。滥用这种行为被认为是很不好的实践,而且还会令你的程序在升级到symfony3时“特别复杂”。

Note

有时,你想要令路由中的某些部分成为“全局配置”。symfony可以利用服务容器参数来做到这一点。参考如何在路由中使用服务容器的参数以了解更多。

特殊的路由参数 

正如你所看到的,每个路由参数或默认值都可以作为控制器方法的参数。此外,有三个参数是特殊的:在你的应用程序中每个都是给你的应用增加一个独特的功能:

_controller
正如你所看到的,这个参数是用来决定“当路由匹配时”要执行哪个控制器的。
_format
用于设置请求格式(request format。了解详情)。
_locale
用于设置请求的locale (了解详情).

控制器命名模式 

如果你使用YAML,XML或PHP的路由配置,那么每个路由都必须有一个_controller参数,用于指示当路匹配时应执行哪个控制器。这个参数使用一个简单的字符串pattern,叫做控制器逻辑名(logical controller name),Symfony用它来映射一个特定的PHP方法或类。此pattern有三个部分,用冒号隔开:

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)
    {
        // ...
    }
}

注意,Symfony在Blog上添加了字符串Controller作为类名(Blog=>BlogController),添加字符串Action作为方法名(show=>showAction)。

你也可以使用它的FQCN类名及方法来指定这个类:AppBundle\Controller\BlogController::showAction。但如果你遵循一些简单的命名约定,逻辑名将更加简洁也更加灵活。

Note

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

路由参数和控制器参数 

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

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

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

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

  • $_locale

  • $year

  • $title

  • $_format

  • $_controller

  • $_route

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

Tip

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

你甚至可以在你的路由中定义额外的信息并在你的控制器中访问它。关于更多信息请阅读 如何从路由向控制器传递额外的信息

生成URL 

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

1
2
3
4
5
6
7
8
9
10
$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')
        );
    }
}

在控制器中你没有继承symfony的父类Controller,那么你不可以使用generateUrl()快捷方法,但你可以使用router的generate()服务方法:

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

在即将到来的部分中,你将学会如何在模板中生成URL地址。

如果你的应用程序前端使用的是ajax请求,你可能希望根据你的路由配置,在JavaScript中生成URL。通过使用FOSJsRoutingBundle,你就可以做到:

1
2
3
4
var url = Routing.generate(
    'blog_show',
    {"slug": 'my-blog-post'}
);

更多信息请阅读这个bundle文档。

生成带有Query Strings的URL 

这个generate方法采用通配符数组来生成URL。但是如果在其中添加了额外的键值对,他们将会被添加成Query Strings来生成一个新的URL:

1
2
3
4
5
$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>
1
2
3
4
5
<a href="<?php echo $view['router']->path('blog_show', array(
    'slug' => 'my-blog-post',
)) ?>">
    Read this blog post.
</a>

生成绝对的URL 

默认情况下,路由器会产生相对的URL(如/blog)。在控制器中,很简单的把generateUrl()方法的第三参数设置成UrlGeneratorInterface::ABSOLUTE_URL即可。

1
2
3
4
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

在模板引擎Twig中,要使用url()函数(生成一个绝对的URL),而不是path()函数(生成一个相对的URL)。在php中,需要要在generateUrl()中传入UrlGeneratorInterface::ABSOLUTE_URL:

1
2
3
<a href="{{ url('blog_show', {'slug': 'my-blog-post'}) }}">
  Read this blog post.
</a>
1
2
3
4
5
<a href="<?php echo $view['router']->url('blog_show', array(
    'slug' => 'my-blog-post',
)) ?>">
    Read this blog post.
</a>

Note

当生成一个绝对URL链接时,所使用的主机自动检测当前使用的Request对象。当生成从web环境外的绝对URL(例如一个控制台命令)这是行不通的。请参见 如何从控制台生成URL 来学习如何解决这个问题。

总结 

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

Keep Going! 

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

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

登录symfonychina 发表评论或留下问题(我们会尽量回复)