# 路由
SF以及所有現代PHP框架都采用“單一入口”的方式。
所謂“單一入口”說的是,一個Web應用,不管要訪問哪個資源和URI,都統一由一個單一的入口文件進行調派。在SF中,這個文件就是`web/app.php`(生產環境)或者`web/app_dev.php`(開發環境)。
在單一入口模式下,用戶在瀏覽器中鍵入類似“`mysite/book/list`”這樣的地址的時候,這樣的請求會被入口文件處理,從中分離出不同的部分。在SF中,這樣的部分可能包括:控制器(一個類)、動作(類方法)、參數等。
怎樣來進行這個分離的動作呢?SF采用的是路由(router)的方法。
在SF中,定義路由有幾種方式。比如注釋方式(annotation)、YML、XML、PHP等。我個人比較喜歡的是用YML的方式。
## 定義入口路徑
不管我們如何設計WEB應用,總是需要定義一個“入口”。
修改或者創建該文件?`src/AppBundle/Resources/config/routing.yml`,使之包含如下內容:
~~~
home:
path: /
defaults: { _controller: AppBundle:Default:index }
~~~
同時修改`app/config/routing.yml`,使之只有如下內容:
~~~
rsywx:
resource: "@AppBundle/Resources/config/routing.yml"
~~~
修改`app/config/routing.yml`的目的是向SF應用表明,我們的路由配置將來自`src/AppBundle/Resources/config/routing.yml`文件。這個文件是一個YML格式的文件,定義了我們應用中所要提供的所有資源的路徑配置。
修改完畢后我們再次訪問應用,瀏覽器將會顯示我們之前看到的SF歡迎頁面:

## 路徑配置
路徑配置的核心包括三個部分:
1. 路徑名。如`home`這樣的一個名稱。該名稱必須在某個路徑配置文件中唯一。
2. 路徑。如`path: /`。該路徑定義了應用能提供的URI。在本例中,我們定義的是入口,也就是通常所說的“首頁”、“主頁”。所以它的路徑是`/`。我們在WEB中用`http(s)://sitename/`對該資源進行訪問。
3. 動作。如`defaults: { _controller: AppBundle:Default:index }`。該動作表明,該路由將調用控制器的某個動作。該控制器位于`src/AppBundle/Controller/DefaultController.php`中,而調用的具體動作是`indexAction`方法。
由此,我們得到此類路徑動作的一個重要約定。SF在尋找動作的時候,會在指定的Bundle(本例中的`AppBundle`目錄,即`src/AppBundle`的控制器目錄(即`src/AppBundle/Controller`)下尋找一個名為“`類名+Controller.php`”的文件(即`DefaultController.php`),并在其中尋找一個名為“`類名+Controller`”的類(即`class DefaultContrller`),再在其中找到一個“`動作名+Action`”的公共方法(即`public function indexAction`)并加以調用。
我們略微看一些這個控制器文件:
~~~
<?php
namespace AppBundle\Controller;
class DefaultController extends Controller
{
public function indexAction(Request $request)
{
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..'),
]);
}
}
~~~
我們在以后還會詳細解釋控制器的編寫。這里只是簡單地提一句:一般情況下,一個控制器中的動作都會返回一個模板的渲染,于是瀏覽器就有內容加以顯示。
## 兩個重要的命令
在深入討論更多路由配置之前,我們先看兩個SF提供的和路由密切相關的命令。
## 路由匹配
總有一天,我們的路由配置會越來越復雜,于是我們會產生疑惑(應用也可能產生bug):某個URI到底匹配哪個路由?其匹配的路由到底是不是我們原先設計中想要的呢?
我們可以使用`php bin/console router:match`命令來對一個URI匹配哪個路由進行調試。比如對`/`路由的調試命令為:
~~~
php bin/console router:match /
~~~
該命令會產生如下輸出:

可見,如我們的設計,`/`匹配了我們定義的`home`路由,它所調用的正是我們規定的`AppBundle:Default:index`動作。
## 路由調試
有時,我們需要知道在應用中到底定義了多少路由,這時我們可以用如下的命令:
~~~
php bin/console debug:router
~~~
該命令將列出所有的路徑名、調用方法(是`POST`、`GET`或者其它還是無所謂)、協議(比如是不是必須要求https)、主機(可以由哪些主機對此訪問)和路徑。
## 更多的路由配置
我們再來看幾個路由,以了解更多的路由配置。
在這個藏書管理程序中,有一個功能是書籍列表(分頁)。該路由定義如下:
~~~
book_list:
path: /books/list/{type}/{key}/{page}
defaults:
page: 1
type: title
key: all
_controller: AppBundle:Book:list
~~~
SF采用`{...}`來標記路徑中的參數。在上例的路由中,其路徑有三個參數:
* `type`:確定書籍列表的類型。一種是列書名,一種是列tag(更多的說明見后續章節);
* `key`:如果`type`是列書名,這里就是書名的開始部分;如果`type`是列tag,這里就是一個tag;
* `page`:確定要顯示第幾頁。
因此用這樣一個單一的路徑,我們可以可以顯示三種不同的書籍列表:
1. 不帶任何參數,或者參數為缺省值,那么列出所有藏書(按照`id`降序,亦即最新登錄的書籍最先展示)的第一頁。
2. 按照書名開頭進行搜索,顯示匹配書名開頭部分的那些書籍。
3. 按照tag進行搜索,顯示匹配tag的那些書籍。
在我的網站中,這些頁面的效果如下所示[1](https://taylorr.gitbooks.io/building-a-web-site-with-symfony/content/05.06%20router.html#fn_1):

我們需要注意的是瀏覽器地址欄顯示的地址。還有就是,雖然這是三個不同的動作,但是它們使用的顯示模板是一樣的。
在該路由的配置中,其`defaults`段和之前的不同。除了按照常規要制定一個控制器和動作外,我們對該路由的路徑中出現的三個參數設置了一個缺省值。所以我們在訪問`books/list`的時候,實際上就是訪問了`/books/list/title/all/1`。
## 只能進行`POST`訪問的路徑
該應用中還有一些路徑是用來處理表單輸入的。對于這樣的路徑,我們不希望用戶在瀏覽器中直接輸入URI而進行誤操作,所以需要對該路徑可以通過怎樣的方法進行訪問加以限制。
比如下面這個為一本書增加tag的路徑:
~~~
tags_add:
path: /books/addtag
defaults: {_controller: AppBundle:Book:tagsAdd}
requirements:
_method: POST
~~~
這里我們設置了路由的一些額外要求。其中的`_method: POST`規定該路由只能通過`POST`方式訪問。
## 對參數的限制
我們有一個書籍詳情的頁面,列出書籍的詳細信息。該路徑定義如下:
~~~
book_detail:
path: /books/{id}.html
defaults: { _controller: AppBundle:Book:detail }
~~~
于是我們就可以用類似`/books/00005.html`這樣的方式來訪問一本書籍。但是這么做有一個小問題。
在我們的數據庫中,一本書的`bookid`有5位,按照約定,它應該都是數字并有前導0,比如`00666`,`01234`等。類似`1234`(位數不夠),`abcd8`(混雜了字母)這樣的參數是不合理的。如果用上述的這個路徑定義,我們訪問`/books/1234.html`的時候,也還會匹配到上面的那個路徑。這樣做不會有什么致命的后果,只是數據庫中無法找到這本書,顯示一個“該書籍找不到”的頁面而已[2](https://taylorr.gitbooks.io/building-a-web-site-with-symfony/content/05.06%20router.html#fn_2)。但是這樣不是很好的方法,如果我們能對路徑中參數加以限制,使得那些不符合要求的參數(和URI)根本不訪問該路由,我們至少解決了部分問題。
于是我們要對該路由中參數`id`加以限制。我們修改上述路由為:
~~~
book_detail:
path: /books/{id}.html
defaults: { _controller: AppBundle:Book:detail }
requirements:
id: \d{5}
~~~
通過一個簡單的正則表達式,我們約定`id`這個參數必須是5位數字,因此類似`1234`,`abcd8`這樣的參數將不會觸發這個路徑。訪問這樣的URI只會出現一個Apache自身的404頁面。
我個人認為,我會比較喜歡這種處理方式。這樣做的一個好處是減少了后臺控制器中的判斷。
## 路由定義的陷阱
隨著我們應用的開發,路由的定義肯定會越來越多。我們有必要強調一些在路由定義時可能會犯的錯誤。
用YML定義的路由,遵循“最先匹配”的原則。某個URI只要符合某個特定的路徑模式就會觸發相應的動作。這么一來就可能會有問題。
假定在我們的路由文件中,有這樣兩個路由:
~~~
display_by_tag:
path: /tag/{tag}
add_tag:
path: /tag/add
requirements:
_method: POST
~~~
如果我們在一個表單中增加了一些tag,然后提交,我們的本意當然是要讓`add_tag`這個路由中指定的動作去執行為一本書增加tag的動作。但是,在這樣的路由配置情形下,首先被匹配的是`display_by_tag`這個路徑,因此我們試圖添加的tag不會真正地保存。
當然,要解決上面提到的問題也有很多方法。我們可以重新規劃路徑,調整路由定義的順序等。
一般而言,路由的設計需要考慮到兩點:
1. 簡單、直觀
2. 越是特殊的路由就要越早定義。
路由是SF中非常核心的一個部件。它可以由其它應用獨立引用。
對于路由的解說,本文只能給出最基本的講解。SF的[官方文檔中對于路由的說明](http://symfony.com/doc/current/book/routing.html)?才是最權威的指南。
本應用完整的[路由文件](https://github.com/taylorren/rsywx_tutorial/blob/master/src/AppBundle/Resources/config/routing.yml)已經上傳。
路由定義完畢后,我們需要開始模板的編寫。
> 1. 我們現在的應用因為只有樣本數據,所以是無法顯示出這樣的結果的。但是我們在后面會看到,即便如此,我們還是可以顯示一個示范的效果。[??](https://taylorr.gitbooks.io/building-a-web-site-with-symfony/content/05.06%20router.html#reffn_1 "Jump back to footnote [1] in the text.")
> 2. 該頁面不是Apache自己的404頁面,而是我們定制的一個頁面。[??](https://taylorr.gitbooks.io/building-a-web-site-with-symfony/content/05.06%20router.html#reffn_2 "Jump back to footnote [2] in the text.")
- 引言
- 1 LAMP
- 1.1 安裝虛擬機
- 1.2 安裝Vagrant
- 1.3 安裝Ubuntu
- 1.4 安裝Apache 2
- 1.5 安裝PHP
- 1.6 安裝MySQL服務器
- 1.7 最后的微調
- 1.8 設置一個虛擬主機
- 1.9 一個趁手的IDE
- 2 Symfony 3和重要構件
- 2.1 Symfony 3
- 2.2 Doctrine
- 2.3 Twig
- 2.4 Composer
- 3 Symfony重要概念
- 3.1 MVC
- 3.2 Bundle/包
- 3.3 Route/路由
- 3.4 Controller/控制器
- 3.5 Entity/實體
- 3.6 Repository/倉庫
- 3.7 Template/模板
- 3.8 Test/測試
- 4 藏書管理程序的結構
- 5 創建應用
- 5.1 建立版本管理
- 5.2 建立數據庫
- 5.3 應用結構
- 5.4 建立數據庫實體
- 5.5 樣本數據
- 5.6 路由
- 5.7 模板
- 5.8 開始編寫首頁
- 5.9 書籍詳情頁面
- 5.10 書籍列表頁面
- 5.11 書籍搜索
- 6 用戶和后臺
- 7 結語