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