1. 程式人生 > >Symfony4中文文件: 路由

Symfony4中文文件: 路由

路由

漂亮的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;

給{佔位符}一個預設值

在前面的例子中, 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')
        );
    }
}
// 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

生成絕對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'}) }}