Zend Framework 2-了解 Router

2018-09-28 20:18 更新

了解 Router

現(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 提供的最重要的路徑類型。

Zend\Mvc\Router\Http\Literal

第一個(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',
                 ),
             ),
         )
     )
 )

Zend\Mvc\Router\Http\Segment

第二常見(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ù),controlleraction。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ù)controlleraction,則只需要跟隨 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)這些 controlleraction 都不存在。這個(gè)路徑仍然能夠匹配成功,但是一個(gè)異常會(huì)被拋出,因?yàn)?Router 無(wú)法找到所請(qǐng)求的資源,最終我們會(huì)收到 404 回應(yīng)。

使用 child_routes 定義的顯式路徑

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

不使用配置結(jié)構(gòu)

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

使用 child_routes 增強(qiáng)結(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ō)就是所有下述路徑都是有效的:

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

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

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

可見(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)單很多。

針對(duì)我們的博客模組的一個(gè)實(shí)用例子

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

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)