鍍金池/ 教程/ PHP/ 了解 Router
了解 Router
回顧博客應用程序
應用 Form 和 Fieldset
編輯數(shù)據(jù)和刪除數(shù)據(jù)
介紹我們第一個“博客” Module
介紹 Zend\Db\Sql 和 Zend\Stdlib\Hydrator
介紹 Service 和 ServiceManager
為不同的數(shù)據(jù)庫后臺做準備

了解 Router

現(xiàn)在我們的模組有了一個非常堅固的基礎。然而,我們并沒有做太多的事情,準確來說,我們做的所有事情僅僅是在一個頁面上顯示所有 Blog 條目而已。在這個章節(jié),你將會學習關于 Router 所有你所需要知道的事情,來創(chuàng)建其他路徑來顯示其中一個博客帖子,添加一個新的博客帖子,和編輯或者刪除現(xiàn)有的博客帖子。

不同的路徑類型

在我們考慮應用程序的細節(jié)之前,先看看 Zend Framework 提供的最重要的路徑類型。

Zend\Mvc\Router\Http\Literal

第一個常見的路徑類型是 Literal(文字) 路徑。和上一個章節(jié)中提到的一樣,文字路徑時一種匹配某個特定字符串的路徑。通常是文字路徑的 URL 例子如下:

為文字路徑進行配置需要你設置好需要匹配的路徑,并且需要你定義一些要使用的默認值,舉例來說哪個 controller 和哪個 action 用以調用。一個文字路徑的簡單配置如下例所示:

 'router' => array(
     'routes' => array(
         'about' => array(
             'type' => 'literal',
             'options' => array(
                 'route'    => '/about-me',
                 'defaults' => array(
                     'controller' => 'AboutMeController',
                     'action'     => 'aboutme',
                 ),
             ),
         )
     )
 )

Zend\Mvc\Router\Http\Segment

第二常見的路徑類型就是 Segment(段)路徑。當你的 url 包含變量參數(shù)時就適用段路徑。這些參數(shù)經(jīng)常用來確認某個您的應用程序里的對象。一些包含參數(shù)的 URL 通常都是段路徑。

配置一個段路徑需要花更多的精力,不過其并不難理解。你需要做的工作一開始都十分相似,你需要定義路徑類型,為了確認請將其設置為 Segment。然后你必須去定義路徑并且對其添加參數(shù)。然后和往常一樣你還要定義要使用的默認值,唯一和先前不同的是你可以定義參數(shù)的默認值。新的部分是你需要定義所謂的 constraints(約束),它會作用于所有的段路徑上,告訴 Router 哪些“規(guī)則”被分別應用于哪些參數(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ù)組包含了正則表達式規(guī)則,分別對應你的路徑的每個參數(shù)。在我們這個示例中正則表達式由兩部分組成,第一個是 \d 代表著“一個數(shù)字”,所以從零到九的任意數(shù)字都符合規(guī)則。第二個部分是 {4},代表著前面的定義必須符合 4 個字符長度。所以用簡單語言來說就是“四位數(shù)字”。

如果你現(xiàn)在調用 URL domain.com/news/archive/123,router 就不能完成匹配,因為我們只支持四位數(shù)字的年份。

你也許會注意到我們沒有為參數(shù) year 定義任何 defaults(默認值)。這是因為目前設定好的參數(shù)是一個 required(必要)參數(shù)。如果這個參數(shù)是 optional(可選)的,那么就必須在路徑定義中加以定義。這可以通過為參數(shù)添加方括號實現(xiàn)。讓我們來修改上述示例路徑來讓參數(shù) year 成為可選項,并且將現(xià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 存在的時候才能存在。

不同的路徑概念

當想著應用程序的整體的時候,你就會清晰意識到有許多種路徑需要被匹配。當編寫這些路徑的時候你有兩種選擇:第一種選擇是付出少一點時間在編寫路徑上,但是在匹配的時候會慢一些;第二種選擇是編寫多一些十分顯式地路徑,這樣匹配會快一些,但是需要多一些工作來對其一一定義。我們來看看兩種方案。

泛用型路徑

泛用型路徑是一種路徑,能匹配許多 URL。你也許還記得這個概念,來自于 Zend Framework 1,那個時候你甚至幾乎不需要考慮路徑問題,因為我們有一條“上帝路徑”用于所有事情。你只需要定義 controller、action 和所有參數(shù)在一個路徑上。

這種方法的一大優(yōu)勢是你可以在開發(fā)中節(jié)省一大堆時間。然而,劣勢就是,匹配這種路徑需要耗費長一點的時間,因為每次匹配都需要檢查很多變量。不過,只要你不要做得太過分,這是一個可行的概念。因為如此, ZendSkeletonApplication(Zend 骨架應用程序)也使用了一個非常泛用的路徑。讓我們來看看一個泛用型路徑:

 '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_-]*',
                 ]
             ),
         )
     )
 )

讓我們仔細看看這個配置中定義了什么:route 部分現(xiàn)在包含兩個可選參數(shù),controlleraction。action 參數(shù)只有在 controller 參數(shù)存在的前提下才是可選的。

defaults 字段看上去也有一點點不一樣。__NAMESPACE__ 總會被用來和 controller 參數(shù)連接在一起。所以舉個例子,當 controller 參數(shù)是“news”時,從 Router 調用的 controller 就會變成 Application\Controller\news;如果參數(shù)是“archive”,那么 Router 會調用控制器 Application\Controller\archive。

defaults 字段的確是十分直接的。而這兩個參數(shù)controlleraction,則只需要跟隨 PHP 標準的傳統(tǒng),必須以 a-z 開頭,大小寫皆可,然后后面可以接上幾乎無限長度的字母、數(shù)字、下劃線或者橫杠。

這種方案的一個巨大的劣勢是,不單是匹配這種路徑會稍微慢一點,還有一點是這種方法根本沒有任何錯誤檢測機制。舉個例子,當你想要調用一個像 domain.com/weird/doesntExist 的 URL 時,controller 就會變成 “Application\Controller\weird”,action 會變成 “doesntExistAction” ??吹矫窒嘈拍膊碌贸鰜磉@些 controlleraction 都不存在。這個路徑仍然能夠匹配成功,但是一個異常會被拋出,因為 Router 無法找到所請求的資源,最終我們會收到 404 回應。

使用 child_routes 定義的顯式路徑

顯式路徑的實現(xiàn)是通過您自行定義所有可能的路徑實現(xiàn)的。若要使用這種方案,你也同樣有兩種選擇。

不使用配置結構

也許最容易理解的編寫顯式路徑的方法就是去編寫許多頂層路徑。所有路徑都有一個顯式名稱,但是有一大堆重復部分。我們不得不每一次都從新定義要使用的默認 controller,而且在配置文件內也沒有任何結構可言。讓我們看看如何能讓這類配置文件更有結構性。

使用 child_routes 增強結構性

另一個定義顯式路徑的選擇就是使用 child_routes(子路徑)。子路徑從他們各自的父母中繼承所有的 options。換句話說就是:當 controller 沒有任何變化時,你不需要重新對其進行定義。我們來看看這個例子:

 '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+'
                         )
                     ),
                 ),
             )
         ),
     )
 )

這個路徑配置可能需要一點詳細解釋。首先我們有一個新的配置條目,稱作 may_terminate。這個屬性定義了其父路徑可以被單獨匹配,不再需要任何子路徑。換句話說就是所有下述路徑都是有效的:

  • /news
  • /news/archive
  • /news/archive/2014
  • /news/42

如果,同時,你若設置了 may_terminate => false,那么其父路徑只能用于所有其 child_routes 的全局默認繼承路徑。換句話說:只有 child_routes 可以被匹配,所以有效路徑剩下:

  • /news/archive
  • /news/archive/2014
  • /news/42

可見父路徑本身不能被匹配。

接下來我們還有一個新條目,叫做 child_routes。著這里我們可以定義追加到父路徑上的新路徑。實際上你自己定義成子路徑的路徑和你在頂層定義的路徑在本質上沒有不同。 唯一會產(chǎn)生區(qū)別的時候在共享默認值的重定義時。

使用這種形式的配置的一大優(yōu)點是,你顯式定義了所有路徑,所以絕對不會遇到和泛用型路徑一樣的問題,例如試圖訪問不存在的控制器。第二個優(yōu)勢就是這種路徑在匹配的時候會比泛用型路徑更快。最后的一個優(yōu)勢就是你可以很輕松的查看所有可能的路徑。

雖然最終這些方案很大程度取決于你的個人喜好,不過請記住,針對顯式路徑的除錯比針對泛用性路徑的除錯會簡單很多。

針對我們的博客模組的一個實用例子

現(xiàn)在我們知道如何配置新路徑了,讓我們先創(chuàng)建一個路徑用來顯示單個數(shù)據(jù)庫里的 Blog。我們希望能夠通過內部 ID 來識別博客帖子。由于那個 ID 是一個變量參數(shù),所以我們需要 Segment 路徑類型的路徑。進一步的,我們還想將這個路徑設置為 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)在我們設置好了一個新路徑來顯示單個博客帖子。我們已經(jīng)對參數(shù) id 規(guī)定了其只能是正整數(shù)。數(shù)據(jù)庫條目的主鍵 ID 通常從 0 開始所以我們的正則表達式 constraints 會稍微復雜一點點?;旧衔覀兏嬖V轉發(fā)器參數(shù) id 字段需要是以一到九的數(shù)字作為開頭,然后可以接上零位到無限多位的數(shù)字。

這個路徑會和其父路徑調用一樣的 controller,但取而代之的它會調用 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

這是因為實際上控制器嘗試訪問 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)建這個模板,并且假設我們會得到一個 Post 對象來查看我們博客的詳細信息。在 /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 模型的實例?,F(xiàn)在對 ListController 進行修改,好讓 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)
         ));
     }
 }

如果你刷新你的應用程序,現(xiàn)在你就能看到我們的 Post 的詳細信息被顯示出來了。不過,我們做的事情中還存在一個小問題。雖然我們將自己的 Service 設定成每當沒有 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 應該準備一些手段來應付 PostService 拋出的 InvalidArgumentException 異常。每當一個無效的 Post 被請求時,我們希望用戶能被重定向到 Post 總覽頁面。讓我們通過添加 try-catch 語句來對 PostService 進行調用:

 <?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 路徑,也就是博客帖子的列表,完美!