Symfony 教學 Chapter 3. Routing | SPACECOWBOY.

You are using an outdated browser. For a faster, safer browsing experience, upgrade for free today.

Symfony 教學 Chapter 3. Routing

Routing

對任何專業的網頁來說,有個漂亮的網址是絕對必須的。這代表說拋棄像是 index.php?article_id=57 的醜陋網址吧,改用這樣的網址 /read/intro-to-symfony

保有彈性是更重要的,如果你想要將頁面的網址從 /blog 改成 /news,你會有多少連結需要被找到並修改他呢?如果你是用 Symfony 的 router,修改網址是很簡單的。

Symfony router 讓你可以定義有創意的網址,並對應到你的網頁應用中的不同區塊。在讀完本篇文章之後,你可以做到以下幾件事:

  • 建立複雜的 route,並對應到相關的 controller
  • 在 template 或 controller 中產生網址字串
  • 從 bundle (或其他方式) 載入 routing 資訊
  • 對你的 route 除錯

Routing 範例

一個 route 代表的是一個網址和一個 controller 之間的對應關係。舉個例子,假設你想在 /blog/my-post/blog/all-about-symfony 網址中,在其對應的 controller 中查詢並顯示部落格的文章。這樣的 route 設定舉例如下:

 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
// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\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'

        // ...
    }
}
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;

$routes = new RouteCollection();
$routes->add('blog_list', new Route('/blog', [
    '_controller' => 'AppBundle:Blog:list',
]));
$routes->add('blog_show', new Route('/blog/{slug}', [
    '_controller' => 'AppBundle:Blog:show',
]));

return $routes;

Thanks to these two routes: 因為有這兩個 route:

  • 如果使用者訪問 /blog,第一個 route 會命中並執行 listAction() 的方法。
  • 如果使用者訪問 /blog/*,第二個 route 會命中並執行 showAction() 的方法,因為 route 的路徑是設定為 /blog/{slug}$slug 這個參數會被傳遞至 showAction() 的方法內。舉個例子,如果使用者訪問 /blog/yay-routing,那 $slug 就會是 yay-routing

每當你的 route 路徑含有 {placeholder} 的部分,這個部分會變成一個萬用字元:他會匹配到 任何 文字。你的 controller 可以包含一個 $placeholder 參數 (萬用字元和參數名稱 必須 要相同)。

Caution

然而,{placeholder} 裡的斜線 / 預設會被忽略,因為 router 會用斜線分隔不同的 placeholder。想了解更多,可以閱讀 How to Allow a "/" Character in a Route Parameter.

每個 route 也有個名字:blog_listblog_show,可以是任何名稱(只要每個名稱是唯一的就好),接下來你會用它產生網址。

Symfony router 的目的是建立網址和 controller 的關聯。在這過程中,你會習得各種建立 route 的技巧,就算是複雜的網址也可以很簡單。

加入 {wildcard} 的限制條件 (requirements)

假設 blog_list 的 route 顯示一個有分頁的部落格文章清單,網址像是 /blog/2/blog/3 表示第2頁和第3頁。如果你設定 route 的路徑為 /blog/{page},你會遇到一個問題:

  • blog_list: /blog/{page} 會比對為 /blog/*
  • blog_show: /blog/{slug} 會比對為 /blog/*

當兩個 route 對應到一樣的網址,第一個被載入的 route 會勝出 (比對成功),也就是說 /blog/yay-routing 會比對到 blog_list 的 route,這不太妙喔!

要修正這個問題,加入一個 requirements 設定,讓 {page} 只允許數字的內容:

 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 Symfony\Component\Routing\Annotation\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;

$routes = new RouteCollection();
$routes->add('blog_list', new Route('/blog/{page}', [
    '_controller' => 'AppBundle:Blog:list',
], [
    'page' => '\d+'
]));

// ...

return $routes;

\d+ 是正規表示式,會比對出一個任意長度的數字,結果如下:

URL Route Parameters
/blog/2 blog_list $page = 2
/blog/yay-routing blog_show $slug = yay-routing

想了解更多其他 requirements 的用法,像是 HTTP method、hostname、和 dynamic expressions,請見 How to Define Route Requirements

給定 {placeholders} 一個預設值

在之前的範例中,blog_list 的網址路徑為 /blog/{page}。如果用戶訪問 /blog/1 那就會匹配到該 route,但如果訪問 /blog 的話,則會無法匹配。只要你的 route 包含一個 {placeholder},那他必須要有個值。

那要如何在訪問 /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 Symfony\Component\Routing\Annotation\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;

$routes = new RouteCollection();
$routes->add('blog_list', new Route(
    '/blog/{page}',
    [
        '_controller' => 'AppBundle:Blog:list',
        'page'        => 1,
    ],
    [
        'page' => '\d+'
    ]
));

// ...

return $routes;

這樣的話,當用戶訪問 /blogblog_list 的 route 將會被匹配到,並且 $page 預設的數值為 1

進階的 Routing 範例

記住所有前述的資訊,來看看這個進階的範例:

 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;

$routes = new RouteCollection();
$routes->add(
    'article_show',
    new Route('/articles/{_locale}/{year}/{slug}.{_format}', [
        '_controller' => 'AppBundle:Article:show',
        '_format'     => 'html',
    ], [
        '_locale' => 'en|fr',
        '_format' => 'html|rss',
        'year'    => '\d+',
    ])
);

return $routes;

就像你看到的,該 route 只會在 {_locale}enfr 並且 {year} 是一個數字的情形下匹配成功,該 route 同時也告訴你可以在 {placeholder} 之間使用小數點 (.) 來取代斜線 (/),匹配該 route 的網址有以下幾個可能:

  • /articles/en/2010/my-post
  • /articles/fr/2010/my-post.rss
  • /articles/en/2013/my-latest-post.html
Note

有時候你想要讓 route 的某部分能被設定在全域環境中,Symfony 中可以設定 service container 的參數來達到這件事,詳細可以閱讀「How to Use Service Container Parameters in your Routes」。

特殊 Routing 參數

就像你看到的,每個 routing 參數或是預設值,最終都可以用在 controller method 中的參數上。此外,還有 4 個特殊的參數,每個都有特定的功能:

  • _controller
    正如你所見,當 route 被匹配後,該參數用於決定要執行哪個 controller。
  • _format
    用於設定請求格式 (read more)
  • _fragment
    用於設定 fragment 錨點,網址中最尾巴以 # 符號開始的部分,用於識別網頁中的某個部分。

    `_fragment` 參數功能加入於 Symfony 3.2。
    
  • _locale
    用於設定語系 (read more)。

帶有結尾斜線的重新導向網址

從歷史的腳色來看,網址是遵從 UNIX 的慣例,在結尾處加入斜線作為資料夾 (像是 https://example.com/foo/),在結尾處移除斜線作為檔案 (https://example.com/foo)。雖然要將兩種網址指向不同的內容是 OK 的,但現今將兩種網址指向同一個內容或是做重新導向已經變為一種共識了。

Symfony 遵從這樣的邏輯,重新導向有或是沒有結尾斜線的網址 (但只限於 GETHEAD 請求):

Route path 如果請求網址為 /foo 如果請求網址為 /foo/
/foo 匹配成功 (200 status response) 匹配不成功 (404 status response)
/foo/ 301 重新導向到 /foo/ 匹配成功 (200 status response)

總結來說,在 route path 中加入結尾斜線是確保網址能運作正常的最佳作法,閱讀 Redirect URLs with a Trailing Slash 的文章,學習如何避免在請求網址包含結尾的斜線但 route path 沒有時發生的 404 錯誤。

Controller Naming Pattern

If you use YAML, XML or PHP route configuration, then each route must have a _controller parameter, which dictates which controller should be executed when that route is matched. This parameter uses a simple string pattern called the logical controller name, which Symfony maps to a specific PHP method and class. The pattern has three parts, each separated by a colon:

bundle:controller:action

For example, a _controller value of AppBundle:Blog:show means:

Bundle Controller Class Method Name
AppBundle BlogController showAction()

The controller might look like this:

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

Notice that Symfony adds the string Controller to the class name (Blog => BlogController) and Action to the method name (show => showAction()).

You could also refer to this controller using its fully-qualified class name and method: AppBundle\Controller\BlogController::showAction. But if you follow some simple conventions, the logical name is more concise and allows more flexibility.

Tip

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. AppBundle\Controller\BlogController).

Note

In addition to using the logical name or the fully-qualified class name, Symfony supports a third way of referring to a controller. This method uses just one colon separator (e.g. service_name:indexAction) and refers to the controller as a service (see How to Define Controllers as Services).

Loading Routes

Symfony loads all the routes for your application from a single routing configuration file: app/config/routing.yml. But from inside of this file, you can load any other routing files you want. In fact, by default, Symfony loads annotation route configuration from your AppBundle's Controller/ directory, which is how Symfony sees our annotation routes:

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;

$routes = new RouteCollection();
$routes->addCollection(
    // second argument is the type, which is required to enable
    // the annotation reader for this resource
    $loader->import("@AppBundle/Controller/", "annotation")
);

return $routes;

For more details on loading routes, including how to prefix the paths of loaded routes, see How to Include External Routing Resources.

Generating URLs

The routing system should also be used to generate URLs. In reality, routing is a bidirectional system: mapping the URL to a controller and a route back to a URL.

To generate a URL, you need to specify the name of the route (e.g. blog_show) and any wildcards (e.g. slug = my-blog-post) used in the path for that route. With this information, any URL can easily be generated:

 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',
            ['slug' => 'my-blog-post']
        );
    }
}
Note

The generateUrl() method defined in the base Controller class is just a shortcut for this code:

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

Generating URLs with Query Strings

The generate() method takes an array of wildcard values to generate the URI. But if you pass extra ones, they will be added to the URI as a query string:

1
2
3
4
5
$this->get('router')->generate('blog', [
    'page' => 2,
    'category' => 'Symfony',
]);
// /blog/2?category=Symfony

Generating URLs from a Template

To generate URLs inside Twig, see the templating article: Linking to Pages. If you also need to generate URLs in JavaScript, see How to Generate Routing URLs in JavaScript.

Generating Absolute URLs

By default, the router will generate relative URLs (e.g. /blog). From a controller, pass UrlGeneratorInterface::ABSOLUTE_URL to the third argument of the generateUrl() method:

1
2
3
4
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

$this->generateUrl('blog_show', ['slug' => 'my-blog-post'], UrlGeneratorInterface::ABSOLUTE_URL);
// http://www.example.com/blog/my-blog-post
Note

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.

Troubleshooting

Here are some common errors you might see while working with routing:

Controller "AppBundle\Controller\BlogController::showAction()" requires that you provide a value for the "$slug" argument.

This happens when your controller method has an argument (e.g. $slug):

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

But your route path does not have a {slug} wildcard (e.g. it is /blog/show). Add a {slug} to your route path: /blog/show/{slug} or give the argument a default value (i.e. $slug = null).

Some mandatory parameters are missing ("slug") to generate a URL for route "blog_show".

This means that you're trying to generate a URL to the blog_show route but you are not passing a slug value (which is required, because it has a {slug}) wildcard in the route path. To fix this, pass a slug value when generating the route:

1
2
3
4
$this->generateUrl('blog_show', ['slug' => 'slug-value']);

// or, in Twig
// {{ path('blog_show', {'slug': 'slug-value'}) }}

Translating Routes

Symfony doesn't support defining routes with different contents depending on the user language. In those cases, you can define multiple routes per controller, one for each supported language; or use any of the bundles created by the community to implement this feature, such as JMSI18nRoutingBundle and BeSimpleI18nRoutingBundle.

Summary

Routing is a system for mapping the URL of incoming requests to the controller function that should be called to process the request. It both allows you to specify beautiful URLs and keeps the functionality of your application decoupled from those URLs. Routing is a bidirectional mechanism, meaning that it should also be used to generate URLs.

Keep Going!

Routing, check! Now, uncover the power of controllers.

See Also

此篇文章主要翻譯自 Symfony 官方文件,並做了些許的修改,原始文章請見此連結


Comments