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