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

介紹 Service 和 ServiceManager

在上一個章節(jié)中我們已經(jīng)學習了如何在 Zend Framework 2 中創(chuàng)建一個簡單的“Hello World”應用程序。這是一個良好易學的開端,但是應用程序本身并沒實現(xiàn)任何事情。在這個章節(jié)我們會將 Service(服務)的概念介紹給您,通過這篇關于 Zend\ServiceManager\ServiceManager 的簡介文章。

什么是 Service

一個 Service 是一個執(zhí)行復雜應用程序邏輯的對象。它是應用程序的一個組成部分,處理所有復雜的事物并且返回一個簡單易懂的結果給您。

為了完成我們想讓 Blog 模塊完成的事情,需要創(chuàng)建一個 Service 來讓它返回我們所需的數(shù)據(jù)。該 Service 會從某些源獲得數(shù)據(jù),然而當我們編寫 Service 時并不會真的在意數(shù)據(jù)源是什么。Service 會根據(jù)一個我們定義的 Interface 來編寫,而未來的數(shù)據(jù)提供者也要根據(jù)這個接口來實現(xiàn)。

編寫 PostService

當編寫一個 Service 的時候,事先定義 Interface 是一項常見的最佳實踐。定義 Interface 可以很好的確保其他程序員能夠方便地為我們的服務編寫擴展,通過他們自己的實現(xiàn)方法。換句話說,他們可以編寫擁有完全一樣的函數(shù)名的 Service,內(nèi)部實現(xiàn)方法完全不同,但是擁有一樣的特定的輸出。

在我們這個例子中,我們需要創(chuàng)建一個 PostService。這意味著首先我們要定義一個 PostServiceInterface。我們的 Service 的任務是將博客帖子的數(shù)據(jù)提供給我們。目前我們先將焦點放在只讀類型的任務。想定義一個函數(shù)來讓我們獲取所有帖子的列表,然后我們再定義一個函數(shù)來獲取單個帖子的內(nèi)容。

首先我們先在 /module/Blog/src/Blog/Service/PostServiceInterface.php 內(nèi)定義接口。

<?php
 // 文件名: /module/Blog/src/Blog/Service/PostServiceInterface.php
 namespace Blog\Service;

 use Blog\Model\PostInterface;

 interface PostServiceInterface
 {
     /**
      * 應該會分會所有博客帖子集,以便我們對其遍歷。數(shù)組中的每個條目應該都是
      * \Blog\Model\PostInterface 接口的實現(xiàn)
      *
      * @return array|PostInterface[]
      */
     public function findAllPosts();

     /**
      * 應該會返回單個博客帖子
      *
      * @param  int $id 應該被返回的帖子的標識符
      * @return PostInterface
      */
     public function findPost($id);
 }

如您所見我們定義了兩個函數(shù)。第一個是 findAllPosts() 用來返回所有的帖子,而第二個函數(shù)是 findPost($id) 用來返回和 $id 標識符參數(shù)匹配的帖子。新奇的是事實上我們定義了一個返回值,然而該值還不存在。我們已經(jīng)默認返回值基本都是 Blog\Model\PostInterface 類型。我們會在稍后定義這個類,目前先創(chuàng)建 PostService

/module/Blog/src/Blog/Service/PostService.php 內(nèi)創(chuàng)建類 PostService,請確保實現(xiàn) PostServiceInterface 和它所依賴的函數(shù)(我們會稍后補全這些函數(shù))。然后您應該有一個類看上去如同下文:

 <?php
 // 文件名: /module/Blog/src/Blog/Service/PostService.php
 namespace Blog\Service;

 class PostService implements PostServiceInterface
 {
     /**
      * {@inheritDoc}
      */
     public function findAllPosts()
     {
         // 文件名: TODO: Implement findAllPosts() method.
     }

     /**
      * {@inheritDoc}
      */
     public function findPost($id)
     {
         // 文件名: TODO: Implement findPost() method.
     }
 }

編寫所需的 Model 文件

因為我們的 PostService 會返回 Model,所以也要創(chuàng)建它們。請確保先為 Model 編寫 Interface!我們來創(chuàng)建 /module/Blog/src/Blog/Model/PostInterface.php/module/Blog/src/Blog/Model/Post.php。首先是 /module/Blog/src/Blog/Model/Post.php

 <?php
 // 文件名: /module/Blog/src/Blog/Model/PostInterface.php
 namespace Blog\Model;

 interface PostInterface
 {
     /**
      * 會返回博客帖子的 ID
      *
      * @return int
      */
     public function getId();

     /**
      * 會返回博客帖子的標題
      *
      * @return string
      */
     public function getTitle();

     /**
      * 會返回博客帖子的文本
      *
      * @return string
      */
     public function getText();
 }

請注意我們在這里只創(chuàng)造了 getter 函數(shù)。這是因為我們目前不關心數(shù)據(jù)是如何進入 Post 類,我們只關心我們?nèi)绾文芡ㄟ^這些 getter 函數(shù)訪問這些屬性。

然后現(xiàn)在我們對照接口創(chuàng)建合適的 Model 文件。請確保設置所需的類屬性并且補全我們的 PostInterface 接口定義的 getter 函數(shù)。盡管我們的接口不關心 setter 函數(shù),我們還是編寫相應的函數(shù)來讓我們的測試數(shù)據(jù)得以寫入。然后您應該有一個類類似下文:

 <?php
 // 文件名: /module/Blog/src/Blog/Model/Post.php
 namespace Blog\Model;

 class Post implements PostInterface
 {
     /**
      * @var int
      */
     protected $id;

     /**
      * @var string
      */
     protected $title;

     /**
      * @var string
      */
     protected $text;

     /**
      * {@inheritDoc}
      */
     public function getId()
     {
         return $this->id;
     }

     /**
      * @param int $id
      */
     public function setId($id)
     {
         $this->id = $id;
     }

     /**
      * {@inheritDoc}
      */
     public function getTitle()
     {
         return $this->title;
     }

     /**
      * @param string $title
      */
     public function setTitle($title)
     {
         $this->title = $title;
     }

     /**
      * {@inheritDoc}
      */
     public function getText()
     {
         return $this->text;
     }

     /**
      * @param string $text
      */
     public function setText($text)
     {
         $this->text = $text;
     }
 }

為我們的 PostService 賦予生機

現(xiàn)在我們擁有我們所需的 Model 文件,可以為 PostService 類賦予生機了。為了讓 Service 層清晰易懂,我們目前只會讓 PostService 直接返回一些事先寫死的(硬編碼的)內(nèi)容。在 PostService 內(nèi)創(chuàng)建一個名為 $data 的屬性,并將其定義為我們的 Model 類型數(shù)組。如下例編輯 PostService

 <?php
 // 文件名: /module/Blog/src/Blog/Service/PostService.php
 namespace Blog\Service;

 class PostService implements PostServiceInterface
 {
     protected $data = array(
         array(
             'id'    => 1,
             'title' => 'Hello World #1',
             'text'  => 'This is our first blog post!'
         ),
         array(
             'id'     => 2,
             'title' => 'Hello World #2',
             'text'  => 'This is our second blog post!'
         ),
         array(
             'id'     => 3,
             'title' => 'Hello World #3',
             'text'  => 'This is our third blog post!'
         ),
         array(
             'id'     => 4,
             'title' => 'Hello World #4',
             'text'  => 'This is our fourth blog post!'
         ),
         array(
             'id'     => 5,
             'title' => 'Hello World #5',
             'text'  => 'This is our fifth blog post!'
         )
     );

     /**
      * {@inheritDoc}
      */
     public function findAllPosts()
     {
         // 文件名: TODO: Implement findAllPosts() method.
     }

     /**
      * {@inheritDoc}
      */
     public function findPost($id)
     {
         // 文件名: TODO: Implement findPost() method.
     }
 }

我們現(xiàn)在有一些數(shù)據(jù)了,來修改 find*() 函數(shù)來使其返回合適的 Model 文件:

 <?php
 // 文件名: /module/Blog/src/Blog/Service/PostService.php
 namespace Blog\Service;

 use Blog\Model\Post;

 class PostService implements PostServiceInterface
 {
     protected $data = array(
         array(
             'id'    => 1,
             'title' => 'Hello World #1',
             'text'  => 'This is our first blog post!'
         ),
         array(
             'id'     => 2,
             'title' => 'Hello World #2',
             'text'  => 'This is our second blog post!'
         ),
         array(
             'id'     => 3,
             'title' => 'Hello World #3',
             'text'  => 'This is our third blog post!'
         ),
         array(
             'id'     => 4,
             'title' => 'Hello World #4',
             'text'  => 'This is our fourth blog post!'
         ),
         array(
             'id'     => 5,
             'title' => 'Hello World #5',
             'text'  => 'This is our fifth blog post!'
         )
     );

     /**
      * {@inheritDoc}
      */
     public function findAllPosts()
     {
         $allPosts = array();

         foreach ($this->data as $index => $post) {
             $allPosts[] = $this->findPost($index);
         }

         return $allPosts;
     }

     /**
      * {@inheritDoc}
      */
     public function findPost($id)
     {
         $postData = $this->data[$id];

         $model = new Post();
         $model->setId($postData['id']);
         $model->setTitle($postData['title']);
         $model->setText($postData['text']);

         return $model;
     }
 }

如您所見,現(xiàn)在我們兩個函數(shù)都擁有合適的返回值了。請注意從技術角度而言目前的實現(xiàn)距離完美還有一大段距離,不過我們會在未來大幅度地改進這個 Service。至少我們目前擁有了一個能運行的 Service 來給我們一些數(shù)據(jù),而這些數(shù)據(jù)吻合我們先前定義的 PostServiceInterface 接口。

將 Service 帶進 Controller

現(xiàn)在我們寫好了 PostService,接下來就想在我們的 Controller(控制器) 內(nèi)訪問這個 Service。為了實現(xiàn)這點我們即將帶出一個新主題,稱為“Dependency Injection”(依賴對象注入),簡稱“DI”。

當我們談論依賴關系注入的時候,其實就是在談如何為類設置依賴對象的問題。最常見的形式,“Constructor Injection”(構造器注入),就是用于指定該類全時需要的所有依賴對象的一種方法。

在這個例子中,我們想讓博客模組 ListController 和我們的 PostService 互動。這意味著 PostService 類是類 ListController 的一個依賴對象(ListController 依賴 PostService),沒有了 PostService,ListController也無法正常運作。為了確保 ListController 總能得到相應的依賴對象,我們會在 ListController 的構造器 __construct() 中事先定義好依賴對象。請將 ListController 修改成下例所示:

<?php
 // 文件名: /module/Blog/src/Blog/Controller/ListController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Mvc\Controller\AbstractActionController;

 class ListController extends AbstractActionController
 {
     /**
      * @var \Blog\Service\PostServiceInterface
      */
     protected $postService;

     public function __construct(PostServiceInterface $postService)
     {
         $this->postService = $postService;
     }
 }

如您所見,我們的 __construct() 函數(shù)現(xiàn)在有了一個必要的參數(shù)?,F(xiàn)在再也不能沒有將符合 PostserviceInterface 接口的類實例作為參數(shù)傳遞的情況下調(diào)用這個類了。如果你現(xiàn)在返回瀏覽器并通過 URL localhost:8080/blog 重新載入您的工程,下面的錯誤信息將映入您的眼簾:

 ( ! ) Catchable fatal error: Argument 1 passed to Blog\Controller\ListController::__construct()
       must be an instance of Blog\Service\PostServiceInterface, none given,
       called in {libraryPath}\Zend\ServiceManager\AbstractPluginManager.php on line {lineNumber}
       and defined in \module\Blog\src\Blog\Controller\ListController.php on line 15

這個錯誤是預料之中的,它告訴你我們的 ListController 期望接收 PostServiceInterface 接口的一個實現(xiàn)作為參數(shù)。

那么我們?nèi)绾未_保 ListController 會接收到這樣的一個實現(xiàn)?解決問題之道在于告訴應用程序如何創(chuàng)建 Blog\Controller\ListController 的實例。如果你還記得我們?nèi)绾蝿?chuàng)造控制器的話,類似的,在模組配置文件中的 invokable 數(shù)組中添加一個條目:

<?php
 // 文件名: /module/Blog/config/module.config.php
 return array(
     'view_manager' => array( /** ViewManager Config */ ),
     'controllers'  => array(
         'invokables' => array(
             'Blog\Controller\List' => 'Blog\Controller\ListController'
         )
     ),
     'router' => array( /** Router Config */ )
 );

一個 invokable 類是一個可以在沒有任何參數(shù)下構造的類。由于我們的 Blog\Controller\ListController 現(xiàn)在需要有構造參數(shù)了,所以需要對此進行一點連帶修改。ControllerManager 負責實例化控制器,也支持使用 factoriesfactory 類用于創(chuàng)建其他類的實例?,F(xiàn)在我們?yōu)槲覀兊?ListController 創(chuàng)建一個 factory 類,先將我們的配置文件按照下例修改:

 <?php
 // 文件名: /module/Blog/config/module.config.php
 return array(
     'view_manager' => array( /** ViewManager Config */ ),
     'controllers'  => array(
         'factories' => array(
             'Blog\Controller\List' => 'Blog\Factory\ListControllerFactory'
         )
     ),
     'router' => array( /** Router Config */ )
 );

如您所見,再也沒有 invokable 鍵了,取而代之的是 factories 鍵。并且,我們的控制器名稱 Blog\Controller\List 已經(jīng)被改成不直接匹配 Blog\Controller\ListController 類,而是調(diào)用一個叫做 Blog\Factory\ListControllerFactory 的類。此時如果你刷新瀏覽器,你會看見另外一個錯誤信息:

 An error occurred
 An error occurred during execution; please try again later.

 Additional information:
 Zend\ServiceManager\Exception\ServiceNotCreatedException

 File:
 {libraryPath}\Zend\ServiceManager\AbstractPluginManager.php:{lineNumber}

 Message:
 While attempting to create blogcontrollerlist(alias: Blog\Controller\List) an invalid factory was registered for this instance type.

這個信息應該相對容易理解。Zend\Mvc\Controller\ControllerManager 正在訪問 Blog\Controller\List,其內(nèi)部表示是 blogcontrollerlist。當它試圖執(zhí)行這個操作的時候發(fā)現(xiàn)要調(diào)用這個控制器,需要先調(diào)用一個 factory 類,然而它并不能找到這個 factory 類,所以產(chǎn)生了我們剛才看見的錯誤。這也是理所當然的,畢竟我們還沒有寫 factory 類,所以接下來我們來干這件事。

編寫一個 Factory 類

在 Zend Framework 2 中的 Factory 類總是需要實現(xiàn) Zend\ServiceManager\FactoryInterface 接口。實現(xiàn)這個類可以讓 ServiceManager 知道函數(shù) createService() 應該被調(diào)用。并且 createService() 其實期望接收一個 ServiceLocatorInterface 的實現(xiàn)作為參數(shù),這樣 ServiceManager 就可以通過先前提到的依賴對象注入來對該參數(shù)進行注入了。首先我們來實現(xiàn) factory 類:

 <?php
 // 文件名: /module/Blog/src/Blog/Factory/ListControllerFactory.php
 namespace Blog\Factory;

 use Blog\Controller\ListController;
 use Zend\ServiceManager\FactoryInterface;
 use Zend\ServiceManager\ServiceLocatorInterface;

 class ListControllerFactory implements FactoryInterface
 {
     /**
      * Create service
      *
      * @param ServiceLocatorInterface $serviceLocator
      *
      * @return mixed
      */
     public function createService(ServiceLocatorInterface $serviceLocator)
     {
         $realServiceLocator = $serviceLocator->getServiceLocator();
         $postService        = $realServiceLocator->get('Blog\Service\PostServiceInterface');

         return new ListController($postService);
     }
 }

現(xiàn)在東西看起來好復雜!讓我們先來看看 $realServiceLocator。當使用一個 Factory 類 時,其本身會被 ControllerManager 調(diào)用,事實上它會將自己$serviceLocator 名義進行注入。然而我們需要真正的 ServiceManager 來訪問 Service 類,這就是為什么我們?nèi)フ{(diào)用 getServiceLocator() 函數(shù)來獲取真正的 ServiceManager

在我們設定好 $realServiceLocator(真正的 ServiceLocator) 之后,去嘗試獲得一個叫做 Blog\Service\PostServiceInterface 的 Service。該操作應該會得到一個匹配 PostServiceInterface 的 Service,然后再將獲得的 Service 傳給 ListController,然后 ListController 再被返回。

請注意盡管我們還沒有注冊一個叫做 Blog\Service\PostServiceInterface 的 Service,也請不要期待有天上掉餡餅那樣自動生成的好事發(fā)生,畢竟我們只是給了 Service 一個接口的名稱。刷新您的瀏覽器,然后就能看見下述錯誤信息:

 An error occurred
 An error occurred during execution; please try again later.

 Additional information:
 Zend\ServiceManager\Exception\ServiceNotFoundException

 File:
 {libraryPath}\Zend\ServiceManager\ServiceManager.php:{lineNumber}

 Message:
 Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for Blog\Service\PostServiceInterface

和我們預想的完全一致。應用程序中的某處,目前是在我們的 factory 類,要求一個叫做 Blog\Service\PostServiceInterface 的 Service,但是卻還不認識這個 Service,所以沒有辦法創(chuàng)建被請求的實例。

注冊 Service

注冊一個 Service 和注冊 Controller 一樣簡單。我們只需要修改 module.config.php 文件,添加一個叫做 service_manager 新鍵,其也擁有 invokablefactories。和我們將兩者包含在 controllers 數(shù)組內(nèi)一樣,具體請參照下例配置文件:

 <?php
 // 文件名: /module/Blog/config/module.config.php
 return array(
     'service_manager' => array(
         'invokables' => array(
             'Blog\Service\PostServiceInterface' => 'Blog\Service\PostService'
         )
     ),
     'view_manager' => array( /** View Manager Config */ ),
     'controllers'  => array( /** Controller Config */ ),
     'router'       => array( /** Router Config */ )
 );

如您所見,現(xiàn)在我們添加了一個新的 Service 來監(jiān)聽名稱 Blog\Service\PostServiceInterface 并且指向我們自己的實現(xiàn)(Blog\Service\PostService)。由于我們的 Service 沒有任何依賴對象,所以我們能夠?qū)⑦@個 Service 添加到 invokable 數(shù)組中?,F(xiàn)在去試試打開瀏覽器并刷新頁面,你應該看不到任何錯誤信息了,而是和上一個教程一樣的頁面內(nèi)容。

在我們的 Controller 中使用 Service

現(xiàn)在讓我們在 ListController 中使用 PostService。要實現(xiàn)這點我們需要復寫默認 indexAction() 函數(shù)并且將 PostService 的值返回到視圖。參照下例修改 ListController

 <?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()
         ));
     }
 }

請注意我們的控制器 import 了另外的類。我們需要 import Zend\View\Model\ViewModel,因為這基本是您的 Controller 會返回的數(shù)據(jù)類型。當返回 ViewModel 的一個實例時,你總能對所謂的“視圖變量”進行賦值。在本示例中我們對一個叫做 $posts 的變量進行了賦值,將其設為 PostService 中的 findAllPosts() 函數(shù)的返回值。在這個特例中返回值是一個 Blog\Model\Post 類的數(shù)組。此時刷新瀏覽器會發(fā)現(xiàn)尚未有任何東西有不同,因為我們很明顯需要先修改我們的視圖文件來顯示我們所需要的數(shù)據(jù)。

注意:事實上你并不一定需要返回 ViewModel 的實例。當您返回一個普通的 php array,它也會隱式地被轉換成 ViewModel。所以簡而言之: return new ViewModel(array('foo' => 'bar'));return array('foo' => 'bar'); 是完全等價的。

訪問視圖變量

若想將變量推送到視圖的時候,有兩種方法實現(xiàn)。要不就是直接像這個代碼 $this->posts,要不就是隱式地像 $posts。兩種方法都是一樣的,然而,隱式調(diào)用 $posts 時程序流會走多一步 __call() 函數(shù)。

我們來修改視圖文件,讓其顯示一個所有博客帖子(由 PostService 返回)的列表:

 <!-- 文件名: /module/Blog/view/blog/list/index.phtml -->
 <h1>Blog</h1>

 <?php foreach ($this->posts as $post): ?>
 <article>
     <h1 id="post<?= $post->getId() ?>"><?= $post->getTitle() ?></h1>
     <p>
         <?= $post->getText() ?>
     </p>
 </article>
 <?php endforeach ?>

此處我們簡單地對 $this->posts 使用一個 foreach 循環(huán)。由于數(shù)組中每一個條目都是 Blog\Model\Post 類型的,所以可以使用其對應的 getter 函數(shù)來獲得我們想要的內(nèi)容。

小結

在本章結束之時,我們已經(jīng)掌握如何和 ServiceManager 進行互動,同時也知道了依賴對象注入是個什么概念,也學會了將我們的 service 返回的變量通過 controller 傳給 view(視圖),然后在視圖腳本中遍歷整個數(shù)組。

在下一個章節(jié)中,我們會簡單了解當我們想從數(shù)據(jù)庫中獲得數(shù)據(jù)時所需要做的事情。