現(xiàn)在我們的模組有了一個(gè)非常堅(jiān)固的基礎(chǔ)。然而,我們并沒(méi)有做太多的事情,準(zhǔn)確來(lái)說(shuō),我們做的所有事情僅僅是在一個(gè)頁(yè)面上顯示所有 Blog
條目而已。在這個(gè)章節(jié),你將會(huì)學(xué)習(xí)關(guān)于 Router
所有你所需要知道的事情,來(lái)創(chuàng)建其他路徑來(lái)顯示其中一個(gè)博客帖子,添加一個(gè)新的博客帖子,和編輯或者刪除現(xiàn)有的博客帖子。
在我們考慮應(yīng)用程序的細(xì)節(jié)之前,先看看 Zend Framework 提供的最重要的路徑類型。
第一個(gè)常見(jiàn)的路徑類型是 Literal
(文字) 路徑。和上一個(gè)章節(jié)中提到的一樣,文字路徑時(shí)一種匹配某個(gè)特定字符串的路徑。通常是文字路徑的 URL 例子如下:
為文字路徑進(jìn)行配置需要你設(shè)置好需要匹配的路徑,并且需要你定義一些要使用的默認(rèn)值,舉例來(lái)說(shuō)哪個(gè) controller 和哪個(gè) action 用以調(diào)用。一個(gè)文字路徑的簡(jiǎn)單配置如下例所示:
'router' => array(
'routes' => array(
'about' => array(
'type' => 'literal',
'options' => array(
'route' => '/about-me',
'defaults' => array(
'controller' => 'AboutMeController',
'action' => 'aboutme',
),
),
)
)
)
第二常見(jiàn)的路徑類型就是 Segment
(段)路徑。當(dāng)你的 url 包含變量參數(shù)時(shí)就適用段路徑。這些參數(shù)經(jīng)常用來(lái)確認(rèn)某個(gè)您的應(yīng)用程序里的對(duì)象。一些包含參數(shù)的 URL 通常都是段路徑。
配置一個(gè)段路徑需要花更多的精力,不過(guò)其并不難理解。你需要做的工作一開(kāi)始都十分相似,你需要定義路徑類型,為了確認(rèn)請(qǐng)將其設(shè)置為 Segment
。然后你必須去定義路徑并且對(duì)其添加參數(shù)。然后和往常一樣你還要定義要使用的默認(rèn)值,唯一和先前不同的是你可以定義參數(shù)的默認(rèn)值。新的部分是你需要定義所謂的 constraints
(約束),它會(huì)作用于所有的段路徑上,告訴 Router
哪些“規(guī)則”被分別應(yīng)用于哪些參數(shù)上。舉例來(lái)說(shuō),一個(gè) id
參數(shù)只允許有屬于 integer
的變量,并且只能剛剛好四位數(shù)字
。一個(gè)示例配置類似下例:
'router' => array(
'routes' => array(
'archives' => array(
'type' => 'segment',
'options' => array(
'route' => '/news/archive/:year',
'defaults' => array(
'controller' => 'ArchiveController',
'action' => 'byYear',
),
'constraints' => array(
'year' => '\d{4}'
)
),
)
)
)
這個(gè)配置文件為 URL 定義了一個(gè)路徑類似 domain.com/news/archive/2014
。如您所見(jiàn),我們的路徑現(xiàn)在包含 :year
部分了。這叫做路徑參數(shù)。段路徑的路徑參數(shù)是以冒號(hào)(":
")為頭跟著一串字符來(lái)定義的;那個(gè)字符串就是參數(shù) name
。
在 constraints
你可以看見(jiàn)我們有另外一個(gè)數(shù)組。這個(gè)數(shù)組包含了正則表達(dá)式規(guī)則,分別對(duì)應(yīng)你的路徑的每個(gè)參數(shù)。在我們這個(gè)示例中正則表達(dá)式由兩部分組成,第一個(gè)是 \d
代表著“一個(gè)數(shù)字”,所以從零到九的任意數(shù)字都符合規(guī)則。第二個(gè)部分是 {4}
,代表著前面的定義必須符合 4 個(gè)字符長(zhǎng)度。所以用簡(jiǎn)單語(yǔ)言來(lái)說(shuō)就是“四位數(shù)字”。
如果你現(xiàn)在調(diào)用 URL domain.com/news/archive/123
,router 就不能完成匹配,因?yàn)槲覀冎恢С炙奈粩?shù)字的年份。
你也許會(huì)注意到我們沒(méi)有為參數(shù) year
定義任何 defaults
(默認(rèn)值)。這是因?yàn)槟壳霸O(shè)定好的參數(shù)是一個(gè) required
(必要)參數(shù)。如果這個(gè)參數(shù)是 optional
(可選)的,那么就必須在路徑定義中加以定義。這可以通過(guò)為參數(shù)添加方括號(hào)實(shí)現(xiàn)。讓我們來(lái)修改上述示例路徑來(lái)讓參數(shù) year
成為可選項(xiàng),并且將現(xiàn)在年份作為默認(rèn)值:
'router' => array(
'routes' => array(
'archives' => array(
'type' => 'segment',
'options' => array(
'route' => '/news/archive[/:year]',
'defaults' => array(
'controller' => 'ArchiveController',
'action' => 'byYear',
'year' => date('Y')
),
'constraints' => array(
'year' => '\d{4}'
)
),
)
)
)
請(qǐng)注意我們現(xiàn)在的路徑的一個(gè)部分是可選的了。不單止參數(shù) year
是可選的,連分離 year
和 URL 串 archive
的斜杠也是可選的了,只有在參數(shù) year
存在的時(shí)候才能存在。
當(dāng)想著應(yīng)用程序的整體的時(shí)候,你就會(huì)清晰意識(shí)到有許多種路徑需要被匹配。當(dāng)編寫這些路徑的時(shí)候你有兩種選擇:第一種選擇是付出少一點(diǎn)時(shí)間在編寫路徑上,但是在匹配的時(shí)候會(huì)慢一些;第二種選擇是編寫多一些十分顯式地路徑,這樣匹配會(huì)快一些,但是需要多一些工作來(lái)對(duì)其一一定義。我們來(lái)看看兩種方案。
泛用型路徑是一種路徑,能匹配許多 URL。你也許還記得這個(gè)概念,來(lái)自于 Zend Framework 1,那個(gè)時(shí)候你甚至幾乎不需要考慮路徑問(wèn)題,因?yàn)槲覀冇幸粭l“上帝路徑”用于所有事情。你只需要定義 controller、action 和所有參數(shù)在一個(gè)路徑上。
這種方法的一大優(yōu)勢(shì)是你可以在開(kāi)發(fā)中節(jié)省一大堆時(shí)間。然而,劣勢(shì)就是,匹配這種路徑需要耗費(fèi)長(zhǎng)一點(diǎn)的時(shí)間,因?yàn)槊看纹ヅ涠夹枰獧z查很多變量。不過(guò),只要你不要做得太過(guò)分,這是一個(gè)可行的概念。因?yàn)槿绱耍?ZendSkeletonApplication(Zend 骨架應(yīng)用程序)也使用了一個(gè)非常泛用的路徑。讓我們來(lái)看看一個(gè)泛用型路徑:
'router' => array(
'routes' => array(
'default' => array(
'type' => 'segment',
'options' => array(
'route' => '/[:controller[/:action]]',
'defaults' => array(
'__NAMESPACE__' => 'Application\Controller',
'controller' => 'Index',
'action' => 'index',
),
'constraints' => [
'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
]
),
)
)
)
讓我們仔細(xì)看看這個(gè)配置中定義了什么:route
部分現(xiàn)在包含兩個(gè)可選參數(shù),controller
和 action
。action
參數(shù)只有在 controller
參數(shù)存在的前提下才是可選的。
defaults
字段看上去也有一點(diǎn)點(diǎn)不一樣。__NAMESPACE__
總會(huì)被用來(lái)和 controller
參數(shù)連接在一起。所以舉個(gè)例子,當(dāng) controller
參數(shù)是“news”時(shí),從 Router
調(diào)用的 controller
就會(huì)變成 Application\Controller\news
;如果參數(shù)是“archive”,那么 Router
會(huì)調(diào)用控制器 Application\Controller\archive
。
defaults
字段的確是十分直接的。而這兩個(gè)參數(shù)controller
和 action
,則只需要跟隨 PHP 標(biāo)準(zhǔn)的傳統(tǒng),必須以 a-z
開(kāi)頭,大小寫皆可,然后后面可以接上幾乎無(wú)限長(zhǎng)度的字母、數(shù)字、下劃線或者橫杠。
這種方案的一個(gè)巨大的劣勢(shì)是,不單是匹配這種路徑會(huì)稍微慢一點(diǎn),還有一點(diǎn)是這種方法根本沒(méi)有任何錯(cuò)誤檢測(cè)機(jī)制。舉個(gè)例子,當(dāng)你想要調(diào)用一個(gè)像 domain.com/weird/doesntExist
的 URL 時(shí),controller
就會(huì)變成 “Application\Controller\weird”,action
會(huì)變成 “doesntExistAction” 。看到名字相信您也猜得出來(lái)這些 controller
和 action
都不存在。這個(gè)路徑仍然能夠匹配成功,但是一個(gè)異常會(huì)被拋出,因?yàn)?Router
無(wú)法找到所請(qǐng)求的資源,最終我們會(huì)收到 404 回應(yīng)。
顯式路徑的實(shí)現(xiàn)是通過(guò)您自行定義所有可能的路徑實(shí)現(xiàn)的。若要使用這種方案,你也同樣有兩種選擇。
也許最容易理解的編寫顯式路徑的方法就是去編寫許多頂層路徑。所有路徑都有一個(gè)顯式名稱,但是有一大堆重復(fù)部分。我們不得不每一次都從新定義要使用的默認(rèn) controller
,而且在配置文件內(nèi)也沒(méi)有任何結(jié)構(gòu)可言。讓我們看看如何能讓這類配置文件更有結(jié)構(gòu)性。
另一個(gè)定義顯式路徑的選擇就是使用 child_routes
(子路徑)。子路徑從他們各自的父母中繼承所有的 options
。換句話說(shuō)就是:當(dāng) controller
沒(méi)有任何變化時(shí),你不需要重新對(duì)其進(jìn)行定義。我們來(lái)看看這個(gè)例子:
'router' => array(
'routes' => array(
'news' => array(
'type' => 'literal',
'options' => array(
'route' => '/news',
'defaults' => array(
'controller' => 'NewsController',
'action' => 'showAll',
),
),
// 定義 "/news" 自身就可以被匹配,不一定需要子路徑
'may_terminate' => true,
'child_routes' => array(
'archive' => array(
'type' => 'segment',
'options' => array(
'route' => '/archive[/:year]',
'defaults' => array(
'action' => 'archive',
),
'constraints' => array(
'year' => '\d{4}'
)
),
),
'single' => array(
'type' => 'segment',
'options' => array(
'route' => '/:id',
'defaults' => array(
'action' => 'detail',
),
'constraints' => array(
'id' => '\d+'
)
),
),
)
),
)
)
這個(gè)路徑配置可能需要一點(diǎn)詳細(xì)解釋。首先我們有一個(gè)新的配置條目,稱作 may_terminate
。這個(gè)屬性定義了其父路徑可以被單獨(dú)匹配,不再需要任何子路徑。換句話說(shuō)就是所有下述路徑都是有效的:
如果,同時(shí),你若設(shè)置了 may_terminate => false
,那么其父路徑只能用于所有其 child_routes
的全局默認(rèn)繼承路徑。換句話說(shuō):只有 child_routes
可以被匹配,所以有效路徑剩下:
可見(jiàn)父路徑本身不能被匹配。
接下來(lái)我們還有一個(gè)新條目,叫做 child_routes
。著這里我們可以定義追加到父路徑上的新路徑。實(shí)際上你自己定義成子路徑的路徑和你在頂層定義的路徑在本質(zhì)上沒(méi)有不同。 唯一會(huì)產(chǎn)生區(qū)別的時(shí)候在共享默認(rèn)值的重定義時(shí)。
使用這種形式的配置的一大優(yōu)點(diǎn)是,你顯式定義了所有路徑,所以絕對(duì)不會(huì)遇到和泛用型路徑一樣的問(wèn)題,例如試圖訪問(wèn)不存在的控制器。第二個(gè)優(yōu)勢(shì)就是這種路徑在匹配的時(shí)候會(huì)比泛用型路徑更快。最后的一個(gè)優(yōu)勢(shì)就是你可以很輕松的查看所有可能的路徑。
雖然最終這些方案很大程度取決于你的個(gè)人喜好,不過(guò)請(qǐng)記住,針對(duì)顯式路徑的除錯(cuò)比針對(duì)泛用性路徑的除錯(cuò)會(huì)簡(jiǎn)單很多。
現(xiàn)在我們知道如何配置新路徑了,讓我們先創(chuàng)建一個(gè)路徑用來(lái)顯示單個(gè)數(shù)據(jù)庫(kù)里的 Blog
。我們希望能夠通過(guò)內(nèi)部 ID 來(lái)識(shí)別博客帖子。由于那個(gè) ID 是一個(gè)變量參數(shù),所以我們需要 Segment
路徑類型的路徑。進(jìn)一步的,我們還想將這個(gè)路徑設(shè)置為 blog
的子路徑:
<?php
// 文件名: /module/Blog/config/module.config.php
return array(
'db' => array( /** DB Config */ ),
'service_manager' => array( /* ServiceManager Config */ ),
'view_manager' => array( /* ViewManager Config */ ),
'controllers' => array( /* ControllerManager Config */ ),
'router' => array(
'routes' => array(
'blog' => array(
'type' => 'literal',
'options' => array(
'route' => '/blog',
'defaults' => array(
'controller' => 'Blog\Controller\List',
'action' => 'index',
),
),
'may_terminate' => true,
'child_routes' => array(
'detail' => array(
'type' => 'segment',
'options' => array(
'route' => '/:id',
'defaults' => array(
'action' => 'detail'
),
'constraints' => array(
'id' => '[1-9]\d*'
)
)
)
)
)
)
)
);
現(xiàn)在我們?cè)O(shè)置好了一個(gè)新路徑來(lái)顯示單個(gè)博客帖子。我們已經(jīng)對(duì)參數(shù) id
規(guī)定了其只能是正整數(shù)。數(shù)據(jù)庫(kù)條目的主鍵 ID 通常從 0 開(kāi)始所以我們的正則表達(dá)式 constraints
會(huì)稍微復(fù)雜一點(diǎn)點(diǎn)?;旧衔覀兏嬖V轉(zhuǎn)發(fā)器參數(shù) id
字段需要是以一到九的數(shù)字作為開(kāi)頭,然后可以接上零位到無(wú)限多位的數(shù)字。
這個(gè)路徑會(huì)和其父路徑調(diào)用一樣的 controller
,但取而代之的它會(huì)調(diào)用 detailAction()
。前往你的瀏覽器并且請(qǐng)求 URL http://localhost:8080/blog/2
,你將會(huì)看到如下錯(cuò)誤信息:
A 404 error occurred
Page not found.
The requested controller was unable to dispatch the request.
Controller:
Blog\Controller\List
No Exception available
這是因?yàn)閷?shí)際上控制器嘗試訪問(wèn) detailAction()
函數(shù),但是這個(gè)函數(shù)尚未存在。所以我們現(xiàn)在立刻去創(chuàng)建它。前往你的 ListController
然后添加 action。返回一個(gè)空白的 ViewModel
然后刷新頁(yè)面:
<?php
// 文件名: /module/Blog/src/Blog/Controller/ListController.php
namespace Blog\Controller;
use Blog\Service\PostServiceInterface;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class ListController extends AbstractActionController
{
/**
* @var \Blog\Service\PostServiceInterface
*/
protected $postService;
public function __construct(PostServiceInterface $postService)
{
$this->postService = $postService;
}
public function indexAction()
{
return new ViewModel(array(
'posts' => $this->postService->findAllPosts()
));
}
public function detailAction()
{
return new ViewModel();
}
}
現(xiàn)在你可以看見(jiàn)那些熟悉的錯(cuò)誤信息了,提示模板無(wú)法被渲染。讓我們立刻創(chuàng)建這個(gè)模板,并且假設(shè)我們會(huì)得到一個(gè) Post
對(duì)象來(lái)查看我們博客的詳細(xì)信息。在 /view/blog/list/detail.phtml
中創(chuàng)建一個(gè)新的視圖文件:
<!-- FileName: /module/Blog/view/blog/list/detail.phtml -->
<h1>Post Details</h1>
<dl>
<dt>Post Title</dt>
<dd><?php echo $this->escapeHtml($this->post->getTitle());?></dd>
<dt>Post Text</dt>
<dd><?php echo $this->escapeHtml($this->post->getText());?></dd>
</dl>
觀察這個(gè)模板,我們可以期望變量 $this->post
是一個(gè) Post
模型的實(shí)例?,F(xiàn)在對(duì) ListController
進(jìn)行修改,好讓 Post
被傳遞出去。
<?php
// 文件名: /module/Blog/src/Blog/Controller/ListController.php
namespace Blog\Controller;
use Blog\Service\PostServiceInterface;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class ListController extends AbstractActionController
{
/**
* @var \Blog\Service\PostServiceInterface
*/
protected $postService;
public function __construct(PostServiceInterface $postService)
{
$this->postService = $postService;
}
public function indexAction()
{
return new ViewModel(array(
'posts' => $this->postService->findAllPosts()
));
}
public function detailAction()
{
$id = $this->params()->fromRoute('id');
return new ViewModel(array(
'post' => $this->postService->findPost($id)
));
}
}
如果你刷新你的應(yīng)用程序,現(xiàn)在你就能看到我們的 Post
的詳細(xì)信息被顯示出來(lái)了。不過(guò),我們做的事情中還存在一個(gè)小問(wèn)題。雖然我們將自己的 Service 設(shè)定成每當(dāng)沒(méi)有 Post
匹配給出的 id
時(shí)會(huì)拋出一個(gè) \InvalidArgumentException
異常,但我們還沒(méi)能利用這個(gè)功能。前往你的瀏覽器并且打開(kāi)這個(gè) URL http://localhost:8080/blog/99
。你會(huì)看見(jiàn)如下錯(cuò)誤信息:
An error occurred
An error occurred during execution; please try again later.
Additional information:
InvalidArgumentException
File:
{rootPath}/module/Blog/src/Blog/Service/PostService.php:40
Message:
Could not find row 99
這看上去還是比較丑陋的,所以我們的 ListController
應(yīng)該準(zhǔn)備一些手段來(lái)應(yīng)付 PostService
拋出的 InvalidArgumentException
異常。每當(dāng)一個(gè)無(wú)效的 Post
被請(qǐng)求時(shí),我們希望用戶能被重定向到 Post 總覽頁(yè)面。讓我們通過(guò)添加 try-catch 語(yǔ)句來(lái)對(duì) PostService
進(jìn)行調(diào)用:
<?php
// 文件名: /module/Blog/src/Blog/Controller/ListController.php
namespace Blog\Controller;
use Blog\Service\PostServiceInterface;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class ListController extends AbstractActionController
{
/**
* @var \Blog\Service\PostServiceInterface
*/
protected $postService;
public function __construct(PostServiceInterface $postService)
{
$this->postService = $postService;
}
public function indexAction()
{
return new ViewModel(array(
'posts' => $this->postService->findAllPosts()
));
}
public function detailAction()
{
$id = $this->params()->fromRoute('id');
try {
$post = $this->postService->findPost($id);
} catch (\InvalidArgumentException $ex) {
return $this->redirect()->toRoute('blog');
}
return new ViewModel(array(
'post' => $post
));
}
}
現(xiàn)在只要你訪問(wèn)一個(gè)無(wú)效的 id
,就會(huì)被重定向到 blog
路徑,也就是博客帖子的列表,完美!
更多建議: