.. index:: single: Routing
对于任何严谨的web应用而言,美观的URL是绝对必须的。
这意味着日渐淘汰的 index.php?article_id=57
这类丑陋的URL要被 /read/intro-to-symfony
取代。
拥有灵活性是更加重要的。你把页面的URL从 /blog
改为 /news
时需要做些什么?
你需要追踪并更新多少链接,才能做出这种改变?如果你使用Symfony的路由,改变起来很容易。
.. index:: single: Routing; Basics
路由 是从URL路径到控制器的映射。假设你想要一条完全匹配 /blog
的路由,以及另一条可以匹配 任何 网址的动态路由,例如 /blog/my-post
或 /blog/all-about-symfony
。
路由可以在YAML,XML和PHP中配置。所有格式都提供相同的功能和性能,因此请选择你喜欢的格式。 如果选择PHP注释,请在应用中运行此命令,以添加对它们的支持:
$ composer require annotations
现在你可以配置路由了:
.. configuration-block:: .. code-block:: php-annotations // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; class BlogController extends AbstractController { /** * 确切的匹配 /blog * * @Route("/blog", name="blog_list") */ public function list() { // ... } /** * 匹配 /blog/* * * @Route("/blog/{slug}", name="blog_show") */ public function show($slug) { // $slug 将等于URL的动态部分 // 如果 URL是 /blog/yay-routing, 那么 $slug='yay-routing' // ... } } .. code-block:: yaml # config/routes.yaml blog_list: path: /blog controller: App\Controller\BlogController::list blog_show: path: /blog/{slug} controller: App\Controller\BlogController::show .. code-block:: xml <!-- config/routes.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" controller="App\Controller\BlogController::list" path="/blog" > <!-- settings --> </route> <route id="blog_show" controller="App\Controller\BlogController::show" path="/blog/{slug}"> <!-- settings --> </route> </routes> .. code-block:: php // config/routes.php use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use App\Controller\BlogController; $routes = new RouteCollection(); $routes->add('blog_list', new Route('/blog', array( '_controller' => [BlogController::class, 'list'] ))); $routes->add('blog_show', new Route('/blog/{slug}', array( '_controller' => [BlogController::class, 'show'] ))); return $routes;
感谢这两条路由:
- 如果用户进入
/blog
,则匹配第一个路由并执行list()
; - 如果用户访问
/blog/*
,则匹配第二个路由并执行show()
。 因为路由路径是/blog/{slug}
,所以与该值匹配的一个$slug
变量会传递给show()
。 例如,如果用户转到/blog/yay-routing
,那么$slug
将等于yay-routing
。
每当路由路径中有 {placeholder}
时,该部分就成为通配符:它匹配 任何 值。
你的控制器现在 也可以 有一个名为 $placeholder
的参数(通配符和参数名称 必须保持一致)。
每个路由同样都有一个内部名称:blog_list
和 blog_show
。
这些可以是任何东西(只要它们都是唯一的)并且还没有任何意义。你稍后将使用它们来 :ref:`生成URL <routing-generate>`。
以其他格式配置路由
每个方法上面的 @Route
称为注释。如果你更愿意使用YAML,XML或PHP配置路由,那没问题!
只需创建一个新的路由文件(例如 routes.xml
),Symfony就会自动使用它。
.. versionadded:: 4.1 在Symfony 4.1中引入了本地化路由的功能。
路由可以本地化以支持每个 :doc:`locale </translation/locale>` 的唯一路径。 Symfony提供了一种方便的方式来声明本地化路由而无需重复。
.. configuration-block:: .. code-block:: php-annotations // src/Controller/CompanyController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; class CompanyController extends AbstractController { /** * @Route({ * "nl": "/over-ons", * "en": "/about-us" * }, name="about_us") */ public function about() { // ... } } .. code-block:: yaml # config/routes.yaml about_us: path: nl: /over-ons en: /about-us controller: App\Controller\CompanyController::about .. code-block:: xml <!-- config/routes.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="about_us" controller="App\Controller\CompanyController::about"> <path locale="nl">/over-ons</path> <path locale="en">/about-us</path> </route> </routes> .. code-block:: php // config/routes.php namespace Symfony\Component\Routing\Loader\Configurator; return function (RoutingConfigurator $routes) { $routes->add('about_us', ['nl' => '/over-ons', 'en' => '/about-us']) ->controller('App\Controller\CompanyController::about'); };
当本地化路由在请求期间被匹配时,Symfony会自动感知应使用哪个本地配置。 以这种方式定义路由也消除了对路由重复注册的需要,这最小化了由定义不一致引起的任何错误的风险。
Tip
如果应用使用完整语言(full language)+语言环境(territory locales)例如 fr_FR
,fr_BE
),
你可以在路由中只使用语言部分(language part)(例如 fr
)。
当你希望为共享相同语言的不同语言环境使用相同的路由路径时,这样处理可以防止必须定义多个路径。
.. versionadded:: 4.2 Symfony 4.2中引入了回退语言部分的功能。
国际化应用的一个常见要求是为所有路由添加一个语言环境前缀。 这可以通过为每个语言环境定义不同的前缀来完成(如果你愿意,可以为默认语言环境设置一个空前缀):
.. configuration-block:: .. code-block:: yaml # config/routes/annotations.yaml controllers: resource: '../../src/Controller/' type: annotation prefix: en: '' # 不要为英语(默认语言环境)添加前缀 nl: '/nl' .. code-block:: xml <!-- config/routes/annotations.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"> <import resource="../src/Controller/" type="annotation"> <!-- don't prefix URLs for English, the default locale --> <prefix locale="en"></prefix> <prefix locale="nl">/nl</prefix> </import> </routes> .. code-block:: php // config/routes/annotations.php use Symfony\Component\Routing\RouteCollection; $routes = $loader->import('../src/Controller/', 'annotation'); // don't prefix URLs for English, the default locale $app->addPrefix('/', array('_locale' => 'en')); $app->addPrefix('/nl', array('_locale' => 'nl')); return $routes;
想象一下, blog_list
路由将包含一个博客帖子的分页列表,
其中包含 /blog/2
和 /blog/3
等第2页和第3页的URL。
如果你将路由的路径更改为 /blog/{page}
,将会造成一个困扰:
- blog_list:
/blog/{page}
会匹配/blog/*
; - blog_show:
/blog/{slug}
同样 会匹配/blog/*
.
当两个路由匹配相同的URL时,会先加载第一条路由。
不幸的是,这意味着 /blog/yay-routing
将匹配到 blog_list
。这就糟糕了!
要解决此问题,可以添加 {page}
通配符只能匹配数字(digits)的需求(requirement):
.. configuration-block:: .. code-block:: php-annotations // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; class BlogController extends AbstractController { /** * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}) */ public function list($page) { // ... } /** * @Route("/blog/{slug}", name="blog_show") */ public function show($slug) { // ... } } .. code-block:: yaml # config/routes.yaml blog_list: path: /blog/{page} controller: App\Controller\BlogController::list requirements: page: '\d+' blog_show: # ... .. code-block:: xml <!-- config/routes.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}" controller="App\Controller\BlogController::list"> <requirement key="page">\d+</requirement> </route> <!-- ... --> </routes> .. code-block:: php // config/routes.php use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use App\Controller\BlogController; $routes = new RouteCollection(); $routes->add('blog_list', new Route('/blog/{page}', array( '_controller' => [BlogController::class, 'list'], ), array( 'page' => '\d+' ))); // ... return $routes;
\d+
是一个匹配任意长度数字的正则表达式。现在:
URL | 路由 | 参数 |
---|---|---|
/blog/2 |
blog_list |
$page = 2 |
/blog/yay-routing |
blog_show |
$slug = yay-routing |
如果你愿意,可以使用语法 {placeholder_name<requirements>}
在每个占位符中内联条件。
此功能使配置更简洁,但当需求比较复杂时,它会降低路由的可读性:
.. configuration-block:: .. code-block:: php-annotations // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; class BlogController extends AbstractController { /** * @Route("/blog/{page<\d+>}", name="blog_list") */ public function list($page) { // ... } } .. code-block:: yaml # config/routes.yaml blog_list: path: /blog/{page<\d+>} controller: App\Controller\BlogController::list .. code-block:: xml <!-- config/routes.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<\d+>}" controller="App\Controller\BlogController::list" /> <!-- ... --> </routes> .. code-block:: php // config/routes.php use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use App\Controller\BlogController; $routes = new RouteCollection(); $routes->add('blog_list', new Route('/blog/{page<\d+>}', array( '_controller' => [BlogController::class, 'list'], ))); // ... return $routes;
.. versionadded:: 4.1 Symfony 4.1中引入了内联规则的功能。
要了解其他路由匹配条件(如HTTP方法、主机名和动态表达式),请参阅 :doc:`/routing/requirements`。
在前面的示例中,blog_list
的路径为 /blog/{page}
。
如果用户访问 /blog/1
,它将匹配。但如果他们访问 /blog
,它将无法匹配。
只要向路由添加了 {占位符}
,该占位符就 必须 有一个值。
那么当用户访问 /blog
时,如何让 blog_list
再次匹配?通过添加一个 默认 值:
.. configuration-block:: .. code-block:: php-annotations // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; class BlogController extends AbstractController { /** * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}) */ public function list($page = 1) { // ... } } .. code-block:: yaml # config/routes.yaml blog_list: path: /blog/{page} controller: App\Controller\BlogController::list defaults: page: 1 requirements: page: '\d+' blog_show: # ... .. code-block:: xml <!-- config/routes.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}" controller="App\Controller\BlogController::list"> <default key="page">1</default> <requirement key="page">\d+</requirement> </route> <!-- ... --> </routes> .. code-block:: php // config/routes.php use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use App\Controller\BlogController; $routes = new RouteCollection(); $routes->add('blog_list', new Route( '/blog/{page}', array( '_controller' => [BlogController::class, 'list'], 'page' => 1, ), array( 'page' => '\d+' ) )); // ... return $routes;
现在,当用户访问 /blog
时, blog_list
路由将会匹配,$page
的值将会默认为 1
。
与匹配条件一样,使用语法 {placeholder_name?default_value}
也可以在每个占位符中内联默认值。
此功能与内联条件兼容,因此你可以在同一个占位符中内联:
.. configuration-block:: .. code-block:: php-annotations // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; class BlogController extends AbstractController { /** * @Route("/blog/{page<\d+>?1}", name="blog_list") */ public function list($page) { // ... } } .. code-block:: yaml # config/routes.yaml blog_list: path: /blog/{page<\d+>?1} controller: App\Controller\BlogController::list .. code-block:: xml <!-- config/routes.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 <\d+>?1}" controller="App\Controller\BlogController::list" /> <!-- ... --> </routes> .. code-block:: php // config/routes.php use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use App\Controller\BlogController; $routes = new RouteCollection(); $routes->add('blog_list', new Route('/blog/{page<\d+>?1}', array( '_controller' => [BlogController::class, 'list'], ))); // ... return $routes;
Tip
要为任何一个占位符提供 null
默认值,就不要在 ?
字符之后添加任何内容(例如 /blog/{page?}
)。
.. versionadded:: 4.1 Symfony 4.1中引入了内联默认值的功能。
随着应用的功能增长,你最终会拥有 很多 路由!要查看所有路由,请运行:
$ php bin/console debug:router
------------------------------ -------- -------------------------------------
Name Method Path
------------------------------ -------- -------------------------------------
app_lucky_number ANY /lucky/number/{max}
...
------------------------------ -------- -------------------------------------
.. index:: single: Routing; Advanced example single: Routing; _format parameter
综合以上这些想法,请浏览此高级示例:
.. configuration-block:: .. code-block:: php-annotations // src/Controller/ArticleController.php // ... class ArticleController extends AbstractController { /** * @Route( * "/articles/{_locale}/{year}/{slug}.{_format}", * defaults={"_format": "html"}, * requirements={ * "_locale": "en|fr", * "_format": "html|rss", * "year": "\d+" * } * ) */ public function show($_locale, $year, $slug) { } } .. code-block:: yaml # config/routes.yaml article_show: path: /articles/{_locale}/{year}/{slug}.{_format} controller: App\Controller\ArticleController::show defaults: _format: html requirements: _locale: en|fr _format: html|rss year: \d+ .. code-block:: xml <!-- config/routes.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}" controller="App\Controller\ArticleController::show"> <default key="_format">html</default> <requirement key="_locale">en|fr</requirement> <requirement key="_format">html|rss</requirement> <requirement key="year">\d+</requirement> </route> </routes> .. code-block:: php // config/routes.php use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use App\Controller\ArticleController; $routes = new RouteCollection(); $routes->add( 'article_show', new Route('/articles/{_locale}/{year}/{slug}.{_format}', array( '_controller' => [ArticleController::class, 'show'], '_format' => 'html', ), array( '_locale' => 'en|fr', '_format' => 'html|rss', 'year' => '\d+', )) ); return $routes;
如你所见,只有当URL的 {_locale}
部分为 en
或 fr
且 {year}
为数字时,此路由才会匹配。
此路由还显示了如何在占位符之间使用点(.
)而不是斜杠(/
)。
与此路由匹配的网址可能如下所示:
/articles/en/2010/my-post
/articles/fr/2010/my-post.rss
/articles/en/2013/my-latest-post.html
特殊的 _format
路由参数
此示例还突出显示了特殊的 _format
路由参数。使用此参数时,匹配的值将成为 Request
对象的“请求格式”。
最终,请求格式用于诸如设置响应的 Content-Type
之类的事情
(例如,json
请求格式会将一个 Content-Type
转换为 application/json
)。
Note
有时你希望使路由的某些部分全局可配置。 Symfony通过利用服务容器参数为你提供了一种方法。 在 ":doc:`/routing/service_container_parameters`" 中阅读有关此内容的更多信息。
如你所见,每个路由参数或默认值最终都可用作控制器方法中的参数。 此外,还有四个特殊参数:每个参数在应用中添加一个独特的功能:
_controller
- 如你所见,此参数用于确定路由匹配时执行的控制器。
_format
- 用于设置请求的格式(:ref:`详细信息 <routing-format-param>`)。
_fragment
- 用于设置片段(fragment)标识符,URL的以
#
字符开头的最后部分为可选配置,用于标识一个文档的部分内容。 _locale
- 用于在请求上设置语言环境(:ref:`详细信息 <translation-locale-url>`)。
从历史上看,URL遵循UNIX约定,即为目录添加尾部斜杠(例如 https://example.com/foo/
),而删除则是引用文件(https://example.com/foo
)。
虽然为两个URL提供不同的内容是可以的,但现在将两个URL视为相同的URL并在它们之间重定向是很常见的。
Symfony遵循这个逻辑,在带有和不带尾部斜杠的URL之间重定向(但仅限于 GET
和 HEAD
请求):
路由路径 | 如果请求URL是 /foo |
如果请求URL是 /foo/ |
/foo |
匹配 (请求状态码 200 ) |
创建 301 并重定向到 /foo |
/foo/ |
创建 301 并重定向到 /foo/ |
匹配 (请求状态码``200``) |
Note
如果你的应用为每个路径(/foo
和 /foo/
)定义了不同的路由,
则不会发生此自动重定向,并且始终匹配正确的路由。
.. versionadded:: 4.1 在Symfony 4.1中引入了从 ``/foo/`` 到 ``/foo`` 的 ``301`` 自动重定向。在之前的Symfony版本中,则是产生 ``404`` 响应。
.. index:: single: Routing; Controllers single: Controller; String naming format
路由中的 controller
值具有一个 CONTROLLER_CLASS::METHOD
格式。
Tip
要引用控制器类中的 __invoke()
方法实现的一个动作,你同样使用完全限定的类名(例如
App\Controller\BlogController
),但不必传递该方法名称。
.. index:: single: Routing; Generating URLs
路由系统也可以生成URL。实际上,路由是双向系统:将URL映射到一个控制器以及从一个路由回到一个URL。
要生成URL,你需要指定路由的名称(例如 blog_show
)以及该路由路径中使用的任何通配符
(例如 slug = my-blog-post
)。
有了这些信息,就可以在控制器中生成一个URL:
class MainController extends AbstractController { public function show($slug) { // ... // /blog/my-blog-post $url = $this->generateUrl( 'blog_show', array('slug' => 'my-blog-post') ); } }
如果需要从服务生成URL,请类型约束 :class:`Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface` 服务:
// src/Service/SomeService.php use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class SomeService { private $router; public function __construct(UrlGeneratorInterface $router) { $this->router = $router; } public function someMethod() { $url = $this->router->generate( 'blog_show', array('slug' => 'my-blog-post') ); // ... } }
.. index:: single: Routing; Generating URLs in a template
generate()
方法采用由通配符组成的数组来生成URI。
但是如果你传递额外的内容,它们将作为查询字符串添加到URI:
$this->router->generate('blog', array( 'page' => 2, 'category' => 'Symfony', )); // /blog/2?category=Symfony
当路由已本地化时,Symfony默认使用当前请求的区域语言来生成URL。
为了生成不同语言环境的URL,你必须在参数数组中传递 _locale
:
$this->router->generate('about_us', array( '_locale' => 'nl', )); // 生成结果: /over-ons
要在 Twig 中生成URL,请参阅模板章节::ref:`templating-pages`。 如果你还需要在JavaScript中生成URL,请参阅 :doc:`/routing/generate_url_javascript`。
.. index:: single: Routing; Absolute URLs
默认情况下,路由会生成相对URL(例如 /blog
)。
要在一个控制器中生成绝对URL,请将 UrlGeneratorInterface::ABSOLUTE_URL
作为第三个参数传递给 generateUrl()
方法:
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
生成绝对URL时使用的主机(host)是根据当前的 Request
对象自动检测得来的。
如果是在Web上下文(web context)外部生成绝对URL(例如在控制台命令中),它将不起作用。
请参阅 :doc:`/console/request_context` 以了解如何解决此问题。
以下是使用路由时可能会遇到的一些常见错误:
Controller "App\Controller\BlogController::show()" requires that you provide a value for the "$slug" argument.
会出现这种情况,是因为你的控制器方法拥有一个参数(例如 $slug
):
public function show($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
路由的URL,但你没有给路由路径中的通配符传递 slug
值
(这是必需的,因为该路由有一个 {slug}
)。
要解决此问题,请在生成路径时传递一个 slug
值:
$this->generateUrl('blog_show', array('slug' => 'slug-value')); // or, in Twig // {{ path('blog_show', {'slug': 'slug-value'}) }}
路由部分了解完毕! 现在,揭示 :doc:`控制器 </controller>` 的能力。
.. toctree:: :maxdepth: 1 :glob: routing/*