在上一個(gè)章節(jié)中我們已經(jīng)學(xué)習(xí)了如何在 Zend Framework 2 中創(chuàng)建一個(gè)簡(jiǎn)單的“Hello World”應(yīng)用程序。這是一個(gè)良好易學(xué)的開端,但是應(yīng)用程序本身并沒實(shí)現(xiàn)任何事情。在這個(gè)章節(jié)我們會(huì)將 Service(服務(wù))的概念介紹給您,通過這篇關(guān)于 Zend\ServiceManager\ServiceManager
的簡(jiǎn)介文章。
一個(gè) Service 是一個(gè)執(zhí)行復(fù)雜應(yīng)用程序邏輯的對(duì)象。它是應(yīng)用程序的一個(gè)組成部分,處理所有復(fù)雜的事物并且返回一個(gè)簡(jiǎn)單易懂的結(jié)果給您。
為了完成我們想讓 Blog
模塊完成的事情,需要?jiǎng)?chuàng)建一個(gè) Service 來讓它返回我們所需的數(shù)據(jù)。該 Service 會(huì)從某些源獲得數(shù)據(jù),然而當(dāng)我們編寫 Service 時(shí)并不會(huì)真的在意數(shù)據(jù)源是什么。Service 會(huì)根據(jù)一個(gè)我們定義的 Interface
來編寫,而未來的數(shù)據(jù)提供者也要根據(jù)這個(gè)接口來實(shí)現(xiàn)。
當(dāng)編寫一個(gè) Service 的時(shí)候,事先定義 Interface
是一項(xiàng)常見的最佳實(shí)踐。定義 Interface
可以很好的確保其他程序員能夠方便地為我們的服務(wù)編寫擴(kuò)展,通過他們自己的實(shí)現(xiàn)方法。換句話說,他們可以編寫擁有完全一樣的函數(shù)名的 Service,內(nèi)部實(shí)現(xiàn)方法完全不同,但是擁有一樣的特定的輸出。
在我們這個(gè)例子中,我們需要?jiǎng)?chuàng)建一個(gè) PostService
。這意味著首先我們要定義一個(gè) PostServiceInterface
。我們的 Service 的任務(wù)是將博客帖子的數(shù)據(jù)提供給我們。目前我們先將焦點(diǎn)放在只讀類型的任務(wù)。想定義一個(gè)函數(shù)來讓我們獲取所有帖子的列表,然后我們?cè)俣x一個(gè)函數(shù)來獲取單個(gè)帖子的內(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
{
/**
* 應(yīng)該會(huì)分會(huì)所有博客帖子集,以便我們對(duì)其遍歷。數(shù)組中的每個(gè)條目應(yīng)該都是
* \Blog\Model\PostInterface 接口的實(shí)現(xiàn)
*
* @return array|PostInterface[]
*/
public function findAllPosts();
/**
* 應(yīng)該會(huì)返回單個(gè)博客帖子
*
* @param int $id 應(yīng)該被返回的帖子的標(biāo)識(shí)符
* @return PostInterface
*/
public function findPost($id);
}
如您所見我們定義了兩個(gè)函數(shù)。第一個(gè)是 findAllPosts()
用來返回所有的帖子,而第二個(gè)函數(shù)是 findPost($id)
用來返回和 $id
標(biāo)識(shí)符參數(shù)匹配的帖子。新奇的是事實(shí)上我們定義了一個(gè)返回值,然而該值還不存在。我們已經(jīng)默認(rèn)返回值基本都是 Blog\Model\PostInterface
類型。我們會(huì)在稍后定義這個(gè)類,目前先創(chuàng)建 PostService
。
在 /module/Blog/src/Blog/Service/PostService.php
內(nèi)創(chuàng)建類 PostService
,請(qǐng)確保實(shí)現(xiàn) PostServiceInterface
和它所依賴的函數(shù)(我們會(huì)稍后補(bǔ)全這些函數(shù))。然后您應(yīng)該有一個(gè)類看上去如同下文:
<?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.
}
}
因?yàn)槲覀兊?PostService
會(huì)返回 Model,所以也要?jiǎng)?chuàng)建它們。請(qǐ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
{
/**
* 會(huì)返回博客帖子的 ID
*
* @return int
*/
public function getId();
/**
* 會(huì)返回博客帖子的標(biāo)題
*
* @return string
*/
public function getTitle();
/**
* 會(huì)返回博客帖子的文本
*
* @return string
*/
public function getText();
}
請(qǐng)注意我們?cè)谶@里只創(chuàng)造了 getter 函數(shù)。這是因?yàn)槲覀兡壳安魂P(guān)心數(shù)據(jù)是如何進(jìn)入 Post
類,我們只關(guān)心我們?nèi)绾文芡ㄟ^這些 getter 函數(shù)訪問這些屬性。
然后現(xiàn)在我們對(duì)照接口創(chuàng)建合適的 Model 文件。請(qǐng)確保設(shè)置所需的類屬性并且補(bǔ)全我們的 PostInterface
接口定義的 getter 函數(shù)。盡管我們的接口不關(guān)心 setter 函數(shù),我們還是編寫相應(yīng)的函數(shù)來讓我們的測(cè)試數(shù)據(jù)得以寫入。然后您應(yīng)該有一個(gè)類類似下文:
<?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;
}
}
現(xiàn)在我們擁有我們所需的 Model 文件,可以為 PostService
類賦予生機(jī)了。為了讓 Service 層清晰易懂,我們目前只會(huì)讓 PostService
直接返回一些事先寫死的(硬編碼的)內(nèi)容。在 PostService
內(nèi)創(chuàng)建一個(gè)名為 $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)在我們兩個(gè)函數(shù)都擁有合適的返回值了。請(qǐng)注意從技術(shù)角度而言目前的實(shí)現(xiàn)距離完美還有一大段距離,不過我們會(huì)在未來大幅度地改進(jìn)這個(gè) Service。至少我們目前擁有了一個(gè)能運(yùn)行的 Service 來給我們一些數(shù)據(jù),而這些數(shù)據(jù)吻合我們先前定義的 PostServiceInterface
接口。
現(xiàn)在我們寫好了 PostService
,接下來就想在我們的 Controller(控制器) 內(nèi)訪問這個(gè) Service。為了實(shí)現(xiàn)這點(diǎn)我們即將帶出一個(gè)新主題,稱為“Dependency Injection”(依賴對(duì)象注入),簡(jiǎn)稱“DI”。
當(dāng)我們談?wù)撘蕾囮P(guān)系注入的時(shí)候,其實(shí)就是在談如何為類設(shè)置依賴對(duì)象的問題。最常見的形式,“Constructor Injection”(構(gòu)造器注入),就是用于指定該類全時(shí)需要的所有依賴對(duì)象的一種方法。
在這個(gè)例子中,我們想讓博客模組 ListController
和我們的 PostService
互動(dòng)。這意味著 PostService
類是類 ListController
的一個(gè)依賴對(duì)象(ListController
依賴 PostService
),沒有了 PostService
,ListController
也無法正常運(yùn)作。為了確保 ListController
總能得到相應(yīng)的依賴對(duì)象,我們會(huì)在 ListController
的構(gòu)造器 __construct()
中事先定義好依賴對(duì)象。請(qǐng)將 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)在有了一個(gè)必要的參數(shù)?,F(xiàn)在再也不能沒有將符合 PostserviceInterface
接口的類實(shí)例作為參數(shù)傳遞的情況下調(diào)用這個(gè)類了。如果你現(xiàn)在返回瀏覽器并通過 URL localhost:8080/blog
重新載入您的工程,下面的錯(cuò)誤信息將映入您的眼簾:
( ! ) 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
這個(gè)錯(cuò)誤是預(yù)料之中的,它告訴你我們的 ListController
期望接收 PostServiceInterface
接口的一個(gè)實(shí)現(xiàn)作為參數(shù)。
那么我們?nèi)绾未_保 ListController
會(huì)接收到這樣的一個(gè)實(shí)現(xiàn)?解決問題之道在于告訴應(yīng)用程序如何創(chuàng)建 Blog\Controller\ListController
的實(shí)例。如果你還記得我們?nèi)绾蝿?chuàng)造控制器的話,類似的,在模組配置文件中的 invokable
數(shù)組中添加一個(gè)條目:
<?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 */ )
);
一個(gè) invokable
類是一個(gè)可以在沒有任何參數(shù)下構(gòu)造的類。由于我們的 Blog\Controller\ListController
現(xiàn)在需要有構(gòu)造參數(shù)了,所以需要對(duì)此進(jìn)行一點(diǎn)連帶修改。ControllerManager
負(fù)責(zé)實(shí)例化控制器,也支持使用 factories
。factory
類用于創(chuàng)建其他類的實(shí)例?,F(xiàn)在我們?yōu)槲覀兊?ListController
創(chuàng)建一個(gè) 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)用一個(gè)叫做 Blog\Factory\ListControllerFactory
的類。此時(shí)如果你刷新瀏覽器,你會(huì)看見另外一個(gè)錯(cuò)誤信息:
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.
這個(gè)信息應(yīng)該相對(duì)容易理解。Zend\Mvc\Controller\ControllerManager
正在訪問 Blog\Controller\List
,其內(nèi)部表示是 blogcontrollerlist
。當(dāng)它試圖執(zhí)行這個(gè)操作的時(shí)候發(fā)現(xiàn)要調(diào)用這個(gè)控制器,需要先調(diào)用一個(gè) factory 類,然而它并不能找到這個(gè) factory 類,所以產(chǎn)生了我們剛才看見的錯(cuò)誤。這也是理所當(dāng)然的,畢竟我們還沒有寫 factory 類,所以接下來我們來干這件事。
在 Zend Framework 2 中的 Factory 類總是需要實(shí)現(xiàn) Zend\ServiceManager\FactoryInterface
接口。實(shí)現(xiàn)這個(gè)類可以讓 ServiceManager 知道函數(shù) createService()
應(yīng)該被調(diào)用。并且 createService()
其實(shí)期望接收一個(gè) ServiceLocatorInterface 的實(shí)現(xiàn)作為參數(shù),這樣 ServiceManager 就可以通過先前提到的依賴對(duì)象注入來對(duì)該參數(shù)進(jìn)行注入了。首先我們來實(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)在東西看起來好復(fù)雜!讓我們先來看看 $realServiceLocator
。當(dāng)使用一個(gè) Factory 類 時(shí),其本身會(huì)被 ControllerManager
調(diào)用,事實(shí)上它會(huì)將自己以 $serviceLocator
名義進(jìn)行注入。然而我們需要真正的 ServiceManager
來訪問 Service 類,這就是為什么我們?nèi)フ{(diào)用 getServiceLocator()
函數(shù)來獲取真正的 ServiceManager
。
在我們?cè)O(shè)定好 $realServiceLocator
(真正的 ServiceLocator) 之后,去嘗試獲得一個(gè)叫做 Blog\Service\PostServiceInterface
的 Service。該操作應(yīng)該會(huì)得到一個(gè)匹配 PostServiceInterface
的 Service,然后再將獲得的 Service 傳給 ListController
,然后 ListController
再被返回。
請(qǐng)注意盡管我們還沒有注冊(cè)一個(gè)叫做 Blog\Service\PostServiceInterface
的 Service,也請(qǐng)不要期待有天上掉餡餅?zāi)菢幼詣?dòng)生成的好事發(fā)生,畢竟我們只是給了 Service 一個(gè)接口的名稱。刷新您的瀏覽器,然后就能看見下述錯(cuò)誤信息:
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
和我們預(yù)想的完全一致。應(yīng)用程序中的某處,目前是在我們的 factory 類,要求一個(gè)叫做 Blog\Service\PostServiceInterface
的 Service,但是卻還不認(rèn)識(shí)這個(gè) Service,所以沒有辦法創(chuàng)建被請(qǐng)求的實(shí)例。
注冊(cè)一個(gè) Service 和注冊(cè) Controller 一樣簡(jiǎn)單。我們只需要修改 module.config.php
文件,添加一個(gè)叫做 service_manager
新鍵,其也擁有 invokable
和 factories
。和我們將兩者包含在 controllers
數(shù)組內(nèi)一樣,具體請(qǐng)參照下例配置文件:
<?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)在我們添加了一個(gè)新的 Service 來監(jiān)聽名稱 Blog\Service\PostServiceInterface
并且指向我們自己的實(shí)現(xiàn)(Blog\Service\PostService
)。由于我們的 Service 沒有任何依賴對(duì)象,所以我們能夠?qū)⑦@個(gè) Service 添加到 invokable
數(shù)組中。現(xiàn)在去試試打開瀏覽器并刷新頁(yè)面,你應(yīng)該看不到任何錯(cuò)誤信息了,而是和上一個(gè)教程一樣的頁(yè)面內(nèi)容。
現(xiàn)在讓我們?cè)?ListController
中使用 PostService
。要實(shí)現(xiàn)這點(diǎn)我們需要復(fù)寫默認(rè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()
));
}
}
請(qǐng)注意我們的控制器 import 了另外的類。我們需要 import Zend\View\Model\ViewModel
,因?yàn)檫@基本是您的 Controller 會(huì)返回的數(shù)據(jù)類型。當(dāng)返回 ViewModel
的一個(gè)實(shí)例時(shí),你總能對(duì)所謂的“視圖變量”進(jìn)行賦值。在本示例中我們對(duì)一個(gè)叫做 $posts
的變量進(jìn)行了賦值,將其設(shè)為 PostService
中的 findAllPosts()
函數(shù)的返回值。在這個(gè)特例中返回值是一個(gè) Blog\Model\Post
類的數(shù)組。此時(shí)刷新瀏覽器會(huì)發(fā)現(xiàn)尚未有任何東西有不同,因?yàn)槲覀兒苊黠@需要先修改我們的視圖文件來顯示我們所需要的數(shù)據(jù)。
注意:事實(shí)上你并不一定需要返回
ViewModel
的實(shí)例。當(dāng)您返回一個(gè)普通的 phparray
,它也會(huì)隱式地被轉(zhuǎn)換成ViewModel
。所以簡(jiǎn)而言之:return new ViewModel(array('foo' => 'bar'));
和return array('foo' => 'bar');
是完全等價(jià)的。
若想將變量推送到視圖的時(shí)候,有兩種方法實(shí)現(xiàn)。要不就是直接像這個(gè)代碼 $this->posts
,要不就是隱式地像 $posts
。兩種方法都是一樣的,然而,隱式調(diào)用 $posts
時(shí)程序流會(huì)走多一步 __call()
函數(shù)。
我們來修改視圖文件,讓其顯示一個(gè)所有博客帖子(由 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 ?>
此處我們簡(jiǎn)單地對(duì) $this->posts
使用一個(gè) foreach
循環(huán)。由于數(shù)組中每一個(gè)條目都是 Blog\Model\Post
類型的,所以可以使用其對(duì)應(yīng)的 getter 函數(shù)來獲得我們想要的內(nèi)容。
在本章結(jié)束之時(shí),我們已經(jīng)掌握如何和 ServiceManager
進(jìn)行互動(dòng),同時(shí)也知道了依賴對(duì)象注入是個(gè)什么概念,也學(xué)會(huì)了將我們的 service 返回的變量通過 controller 傳給 view(視圖),然后在視圖腳本中遍歷整個(gè)數(shù)組。
在下一個(gè)章節(jié)中,我們會(huì)簡(jiǎn)單了解當(dāng)我們想從數(shù)據(jù)庫(kù)中獲得數(shù)據(jù)時(shí)所需要做的事情。
更多建議: