路由
漂亮的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_list 和 blog_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} 部分為 en 或 fr 且 {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-Type為application/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'}) }}