2

路由

漂亮的URL是任何严谨的Web应用程序所必须的. 这意味着像 index.php?article_id=57 这样丑陋的URL要被 /read/intro-to-symfony 所取代.

具有灵活性更加重要. 如果你需要将 /blog 更改为 /news , 需要做些什么? 你需要搜索并更新多少链接才能做出这种改动? 如果你使用的是Symfony的路由, 更改将是很简单的.

创建路由

路由是从URL到控制器的映射, 假如你想要一个路由完全匹配 /blog 和另外更多可匹配任何像 /blog/my-post/blog/all-about-symfony URL的动态路由.

路由可以在YAML, XML和PHP. 所有格式都提供相同的功能和性能, 因此可选择你喜欢的格式. 如果你选择PHP annotations, 请在你的应用程序中运行一次此命令以添加对它们的支持:

$ composer require annotations

现在你可以配置路由:

Annotations

// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * Matches /blog exactly
     *
     * @Route("/blog", name="blog_list")
     */
    public function list()
    {
        // ...
    }

    /**
     * Matches /blog/*
     *
     * @Route("/blog/{slug}", name="blog_show")
     */
    public function show($slug)
    {
        // $slug will equal the dynamic part of the URL
        // e.g. at /blog/yay-routing, then $slug='yay-routing'

        // ...
    }
}

YAML

# config/routes.yaml
blog_list:
    path:     /blog
    controller: App\Controller\BlogController::list

blog_show:
    path:     /blog/{slug}
    controller: App\Controller\BlogController::show
    

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>

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_listblog_show . 这些可以是任意内容 ( 只要每个都是唯一的 ) 并且需要无任何特别含义. 稍后你将使用它们来生成URL.

其他格式的路由

每个方法上面的 @Route 称为 annotation. 如果你更愿意使用YAML, XML或PHP配置路由, 那没问题! 只需创建一个新的路由文件 ( 例如 routes.xml ) , Symfony就会自动使用它.

本地化路由(i18n)

路由可以本地化地为每个区域提供唯一的路径. Symfony提供了一种简便的方式来声明本地化路由而无重复.

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

YAML

# config/routes.yaml
about_us:
    path:
        nl: /over-ons
        en: /about-us
    controller: App\Controller\CompanyController::about
    

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>

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会自动识别请求期间应使用哪个区域的路由设置. 以这种方式定义路由避免了对路由重复注册的需要, 最小化了由定义不一致引起的任何错误的风险.

为所有路由添加前缀是国际化应用程序的一个常见需求. 这样可以通过为每个语言环境定义不同的路径前缀来完成 ( 如果愿意, 可以为默认语言设置一个空前缀 ):

YAML

# config/routes/annotations.yaml
controllers:
    resource: '../../src/Controller/'
    type: annotation
    prefix:
        en: '' # don't prefix URLs for English, the default locale
        nl: '/nl'
        

添加 {通配符} 条件

想象一下, 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} 通配符用来只匹配数字:

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

YAML

# config/routes.yaml
blog_list:
    path:      /blog/{page}
    controller: App\Controller\BlogController::list
    requirements:
        page: '\d+'

blog_show:
    # ...

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>

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 Route Parameters
/blog/2 blog_list $page = 2
/blog/yay-routing blog_show $slug = yay-routing

如果你愿意, 可以在每个占位符中使用语法 {placeholder_name<requirements>} . 此功能使配置更简洁, 但当需求复杂时, 它会降低路由可读性:

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

YAML

# config/routes.yaml
blog_list:
    path:      /blog/{page<\d+>}
    controller: App\Controller\BlogController::list

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>

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;

要了解其他路由条件 ( 如HTTP方法, 主机名和动态表达式 ) 请参阅 How to Define Route Requirements

给{占位符}一个默认值

在前面的例子中, blog_list 的路径为 /blog/{page} . 如果用户访问 /blog/1 , 则会匹配. 如果用户访问 /blog , 将无法匹配. 只要向路由路径添加了 {占位符} , 它就必须有值.

那么当用户访问 /blog 时, 如何让 blog_list 再次匹配呢? 通过添加一个 默认 值:

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

YAML

# config/routes.yaml
blog_list:
    path:      /blog/{page}
    controller: App\Controller\BlogController::list
    defaults:
        page: 1
    requirements:
        page: '\d+'

blog_show:
    # ...
    

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>

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} 也可以在每个占位符中内联默认值. 此功能与内联条件兼容, 因此你可以在一个占位符中内联:

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

YAML

# config/routes.yaml
blog_list:
    path:      /blog/{page<\d+>?1}
    controller: App\Controller\BlogController::list

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>

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;
占位符变量的值若是 null 变量, 则需要在通配符最后添加 ? 字符. ( 例如 /blog/{page?} ) .

全部路由列表

随着你应用程序的健壮, 最终会有大量的路由被定义! 要查看所有内容, 请运行命令:

$ php bin/console debug:router

------------------------------ -------- -------------------------------------
 Name                           Method   Path
------------------------------ -------- -------------------------------------
 app_lucky_number                 ANY    /lucky/number/{max}
 ...
------------------------------ -------- -------------------------------------

高级路由示例

请查看高级示例:

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

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+

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>

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} 部分为 enfr{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请求格式会转换 Content-Typeapplication/json )

特殊路由参数

如你所见, 每个路由参数或默认值最终都可以作为控制器方法的参数. 此外, 还有四个特殊参数: 每个参数在应用程序中具有独特的功能:

_controller

   用于确定路由匹配时执行的控制器

_format

   用于设置请求格式 ( 阅读更多 )

_fragment

   用于设置fragment identifier, URL的最后可选部分, 以 # 字符开头, 用于标识文档的某一部分.

_locale

   用于在请求上设置区域 ( 阅读更多 )

尾部斜杠重定向URL

从历史上看, URL遵循UNIX约定, 即为路径添加尾部斜杠 ( 例如 https://example.com/foo/ ) , 当删除斜杠时将作为文件引用 ( https://example.com/foo ) . 虽然为两个URL提供不同的内容是可以的, 但现在将两个URL视为相同的URL并在他们之间重定向是很常见的.

Symfony遵循这个逻辑, 在带斜杠和不带斜杠的URL之间重定向 ( 但仅限于GET和HEAD请求 ):

Route path If the requested URL is /foo If the requested URL is /foo/
/foo It matches (200 status response) It makes a 301 redirect to /foo
/foo/ It makes a 301 redirect to /foo/ It matches (200 status response)
如果你的应用程序为每个路径 ( /foo/foo/ ) 定义了不同的路由, 则不会发生自动重定向, 并且始终匹配正确的路由.

在Symfony4.1中引入了从 /foo//foo 的自动301重定向. 在之前的Symfony版本中, 会响应404.

控制器命名模式

路由中的控制器格式非常简单 CONTROLLER_CLASS::METHOD .

To refer to an action that is implemented as the __invoke() method of a controller class, you do not have to pass the method name, but can just use the fully qualified class name (e.g. AppControllerBlogController).

生成URL

路由系统也可以生成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, 注入 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')
        );
        // ...
    }
}

使用查询字符串生成URL

generate() 方法采用通配符数组来生成URI. 但是如果你传递额外值, 他们将作为查询字符串添加到URI中.

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

生成本地化URL

路由本地化时, Symfony默认使用当前请求区域来生成URL. 为了生成不同语言环境的URL, 你必须在parameters数组中传递 _locale :

$this->router->generate('about_us', array(
    '_locale' => 'nl',
));
// generates: /over-ons

从模板中生成URL

要在Twig中生成URL: 请参阅模板章节. 如果你需要在JavaScript中生成URL, 请参阅 How to Generate Routing URLs in JavaScript

生成绝对URL

默认情况下, 路由将生成相对URL ( 例如 /blog ) . 在控制器中, 将 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
The host that's used when generating an absolute URL is automatically detected using the current Request object. When generating absolute URLs from outside the web context (for instance in a console command) this doesn't work. See How to Generate URLs from the Console to learn how to solve this problem.

错误排除

以下是使用路由时可能会遇到的一些常见错误:

Controller "AppControllerBlogController::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'}) }}


dreamans
725 声望847 粉丝

专注PHP、Go、微服务、数据库、分布式系统、算法等后端技术。