控制器
对于任何严谨的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 / 精确匹配了/blog
*
* @Route("/blog", name="blog_list")
*/
public function listAction()
{
// ...
}
/**
* Matches /blog/* / 匹配的是/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
,则第一个路由被匹配而listAction()
会被执行; - 如果用户来到
/blog/*
, 则第二个路由被匹配而showAction()
将被执行。因为路由的路径是/blog/{slug}
, 有个$slug
变量被传到了能够匹配该值的showAction()
中。例如,如果用户来到/blog/yay-routing
,那么$slug
即等于yay-routing
。
只要在你的路由路径(route path)中包含有 {placeholder}
,它便成为一个通配符:它可以匹配 任何 值。你的控制器从现在起 也 可以有一个名为 $placeholder
的参数(此通配符和参数 必须 匹配)。
每个路由都有一个内部名称: blog_list
和 blog_show
。这些名称可以是任何内容 (只要唯一即可) 而且不必赋予任何特别的含意。后面,你将使用它来生成Url。
这就是Symfony路由的目标:把一个请求的URL映射到控制器中。接下来,你将学习到所有类型的技巧,可以令极度复杂的URL在映射时变得容易。
添加{通配符}条件 ¶
设想 blog_list
路由将包含博客主题带有分页的一个列表,对于第2和第3页有类似 /blog/2
和 /blog/3
这样的URL。如果你把路由路径改为 /blog/{page}
,会出现问题:
- blog_list:
/blog/{page}
将匹配/blog/*
; - blog_show:
/blog/{slug}
将 同样 匹配/blog/*
。
当两个路由匹配同一个URL时,第一个 被加载的路由胜出。不幸的是,这也意味着 /blog/yay-routing
将匹配到 blog_list
。不太好!
要修复这个,添加一个 requirement(条件),以便 {page}
通配符能够 仅 匹配数字(digits):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 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)
{
// ...
}
} |
1 2 3 4 5 6 7 8 9 |
# app/config/routing.yml
blog_list:
path: /blog/{page}
defaults: { _controller: AppBundle:Blog:list }
requirements:
page: '\d+'
blog_show:
# ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!-- 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> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 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 method, hostname 以及 dynamic expressions(动态表达式) - 参考 如何定义路由条件。
给{占位符}一个默认值 ¶
前例中,blog_list
路由的路径是 /blog/{page}
。如果用户访问 /blog/1
,则会匹配。但如果他们访问的是 /blog
,就将 无法 匹配到。一旦你添加了某个 {占位符}
到路由中,它就 必须 得有一个值。
那么当用户访问 /blog
时,你如何才能令 blog_list
再一次匹配呢?添加一个 默认 值即可:
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/{page}", name="blog_list", requirements={"page": "\d+"})
*/
public function listAction($page = 1)
{
// ...
}
} |
1 2 3 4 5 6 7 8 9 |
# app/config/routing.yml
blog_list:
path: /blog/{page}
defaults: { _controller: AppBundle:Blog:list, page: 1 }
requirements:
page: '\d+'
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_list" path="/blog/{page}">
<default key="_controller">AppBundle:Blog:list</default>
<default key="page">1</default>
<requirement key="page">\d+</requirement>
</route>
<!-- ... -->
</routes> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 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_list
路由会匹配,并且 $page
路由参数会默认取值为 1
。
高级路由示例 ¶
贯穿所有内容,查看下例:
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}/{slug}.{_format}",
* defaults={"_format": "html"},
* requirements={
* "_locale": "en|fr",
* "_format": "html|rss",
* "year": "\d+"
* }
* )
*/
public function showAction($_locale, $year, $slug)
{
}
} |
1 2 3 4 5 6 7 8 |
# 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+ |
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}/{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> |
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}/{slug}.{_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
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(控制器方法名) |
---|---|---|
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添加了字符串 Controller
到类名中 (Blog
=> BlogController
) 同时添加了 Action
到方法名中 (show
=> showAction()
)。
你也可以使用类的FQCN来引用类名和方法: AppBundle\Controller\BlogController::showAction
。但如果你遵循了是简便方法,逻辑命名是非常轻巧和灵活的。
Tip
若要引用一个在控制器类中实现了 __invoke()
方法的action,你不可以传入方法名,只需使用FQCN类名足矣 (如 AppBundle\Controller\BlogController
)。
Note
除了使用逻辑名或FQCN类名之外,Symfony还支持第三种方式来引用控制器。此方法仅使用一个冒号 (如 service_name:indexAction
) 把控制器作为服务来引入 (参考 如何把控制器定义为服务)。
加载路由 ¶
Symfony 从一个 单一的 路由配置文件: app/config/routing.yml
中加载你的程序中的全部路由。但从这个文件中,你可以加载任何一个 其他的 路由文件。实际上,Symfony默认从你 AppBundle 中的 Controller/
目录中加载annotation路由配置,这就是为何Symfony能够看到我们的annotation路由:
1 2 3 4 |
# app/config/routing.yml
app:
resource: "@AppBundle/Controller/"
type: annotation |
1 2 3 4 5 6 7 8 9 10 |
<!-- 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> |
1 2 3 4 5 6 7 8 9 10 11 |
// 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-post
) 的值。有了这些信息,任何URL皆可轻松生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MainController extends Controller
{
public function showAction($slug)
{
// ...
// /blog/my-blog-post
$url = $this->generateUrl(
'blog_show',
array('slug' => 'my-blog-post')
);
}
} |
Note
定义在
Controller
基类中的 generateUrl()
方法是以下代码的快捷方式:
1 2 3 4 |
$url = $this->container->get('router')->generate(
'blog_show',
array('slug' => 'my-blog-post')
); |
生成带有Query字符串的URL ¶
generate()
方法接收通配符的值的数组,以生成URI。但如果你传入额外的值,它们将被添加到URI中,作为query string(查询字符串):
1 2 3 4 5 |
$this->get('router')->generate('blog', array(
'page' => 2,
'category' => 'Symfony'
));
// /blog/2?category=Symfony |
从模板中生成URL ¶
在Twig中生成URL,参考模板文章: 链到页面。如果你需要在JavaScript中生成URL,参考 如何在JavaScript中生成路由链接。
生成绝对URL ¶
默认时,路由生成相对链接 (如 /blog
)。在控制器中把 UrlGeneratorInterface::ABSOLUTE_URL
作为 generateUrl()
方法的第三个参数传入:
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 |
Note
用于生成绝对链接的host主机名是通过 Request
对象自动侦测的。若要从web上下文之外来生成绝对URL (例如在命令行中),它不会工作。参考 如何在命令行中生成URL 以了解如何解决此问题。
除错 ¶
这里是一些你在使用路由时常见的报错信息:
Controller "AppBundleControllerBlogController::showAction()" requires that you provide a value for the "$slug" argument.
这个报错仅在你的控制器方法有一个参数时才会发生 (如 $slug
):
1 2 3 4 |
public function showAction($slug)
{
// ..
} |
但你的路由路径中 并没有 那个 {slug}
通配符 (如,它是 /blog/show
)。可添加 {slug}
到路由路径中: /blog/show/{slug}
或者给予参数一个默认值 (即 $slug = null
)。
Some mandatory parameters are missing ("slug") to generate a URL for route "blog_show".
这说明你正在为 blog_show
路由生成链接,但你 并没有 在路由的路径中传入一个 slug
值 (它是必须的,因为路由有 {slug}
通配符)。解决办法是是,在生成路由(URL)时传入 slug
值:
1 2 3 4 |
$this->generateUrl('blog_show', array('slug' => 'slug-value'));
// or, in Twig / 或者在Twig中
// {{ path('blog_show', {'slug': 'slug-value'}) }} |
路由的翻译 ¶
Symfony不支持基于语种以不同内容来定义路由。在那些场合下,你可以为每个控制器定义多个路由,每一个路由支持其所对应语言;或者使用社区的三方bundle来实现此功能,比如 JMSI18nRoutingBundle 和 BeSimpleI18nRoutingBundle。
总结 ¶
路由是一个将传入的请求之URL映射到用来处理该请求的控制器函数的系统。它允许你指定一个美观的URL,并使程序的功能性与URL“解耦”。路由是一个双向架构,意味着它也可以用来生成URL。
Keep Going! ¶
路由,核对完毕!现在,去解封 控制器 的威力。