数据库
现在我们已经建立好了一个拥有控制器及其方法以及显示层脚本的模块,现在就让我们来看看应用的模型层部分。记住模型层是处于应用中处理核心问题的部分(所以也被称为“业务层”),换句话说,就是处理数据库。我们将会使用 Zend\Db\TableGateway\TableGateway 这个类,专门对数据库进行增、删、改、查的作用。
我们通过PHP的PDO方式来连接MySQL,然后创建一个名为 “zf2tutorial”的数据库,执行下面的 SQL 语句来创建封面表,并且插入一些数据进去。
CREATE TABLE album (
id int(11) NOT NULL auto_increment,
artist varchar(100) NOT NULL,
title varchar(100) NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO album(artist, title)
VALUES ('The Military Wives', 'In My Dreams');
INSERT INTO album(artist, title)
VALUES ('Adele', '21');
INSERT INTO album(artist, title)
VALUES ('Bruce Springsteen', 'Wrecking Ball (Deluxe)');
INSERT INTO album(artist, title)
VALUES ('Lana Del Rey', 'Born To Die');
INSERT INTO album(artist, title)
VALUES ('Gotye', 'Making Mirrors');
(当写此片教程的时候,这些测试数据是 Amazon 英国上卖的最好的唱片列表。)
我们现在已经在数据库中有一些数据了,然后我们可以针对这些数据写一个简单的模型。
模型文件
Zend Framework 并不提供一个 Zend\Model 组件作为你业务逻辑的模型,而是让你自行决定如何处理。其实这里是有很多根据你要求来选择的各种组件。一个不错的方法就是在你的应用中每个模型层代表每个实体并且使用映射对象将这些实体保存进数据库(译者注:此处的意思应该是指将信息以对象的方式存进数据库,而不是每条记录的方式存储?)。另外一个方式就是用 ORM 或者 Propel 等 ORM 方式存储。
因为是教程,我们就只使用 Zend\Db\TableGateway\TableGateway 类每个封面对象就是一个封面对象(或者认为是实体)来创建一个 AlbumTable 类这样的简单模型。它是基于 Table DataGateway 设计模式而实现的一种方式。因为我们考虑到在一些大的系统里面 Table Data Gateway 这种设计模式还是有局限性的。所以你可以扩展 Zend\Db\TableGateway\AbstractTableGateway 这个类来进行数据存储。但是一般也不咋用!
现在就让我们从模型层文件夹的封面实体类开始吧:
//module/Album/src/Album/Model/Album.php:
namespace Album\Model;
class Album
{
public $id;
public $artist;
public $title;
public function exchangeArray($data)
{
$this->id = (isset($data['id'])) ? $data['id'] :null;
$this->artist =(isset($data['artist'])) ? $data['artist'] : null;
$this->title = (isset($data['title'])) ? $data['title'] :null;
}
}
我们的封面实体是个简单的PHP类。为了能够和 Zend\Db 的 TableGateway 类一起使用,我们需要实现 exchangeArray() 方法。这个方法只是简单的对$data 参数进行数据拷贝到类的属性。我们稍后会对表单的数据进行过滤。
但是首先,我们能够确定这个封面模型是否可以按照我们预期的运行呢?现在就让我们写一些测试来确认下。
//tests/module/Album/src/Album/Model/AlbumTest.php:
namespace Album\Model;
usePHPUnit_Framework_TestCase;
class AlbumTest extendsPHPUnit_Framework_TestCase
{
public function testAlbumInitialState()
{
$album = new Album();
$this->assertNull($album->artist,'"artist" should initially be null');
$this->assertNull($album->id,'"id" should initially be null');
$this->assertNull($album->title,'"title" should initially be null');
}
public functiontestExchangeArraySetsPropertiesCorrectly()
{
$album = new Album();
$data = array('artist' => 'some artist',
'id' => 123,
'title' => 'some title');
$album->exchangeArray($data);
$this->assertSame($data['artist'],$album->artist, '"artist" was not set correctly');
$this->assertSame($data['id'],$album->id, '"title" was not set correctly');
$this->assertSame($data['title'],$album->title, '"title" was not set correctly');
}
public functiontestExchangeArraySetsPropertiesToNullIfKeysAreNotPresent()
{
$album = new Album();
$album->exchangeArray(array('artist'=> 'some artist',
'id' => 123,
'title' => 'some title'));
$album->exchangeArray(array());
$this->assertNull($album->artist,'"artist" should have defaulted to null');
$this->assertNull($album->id,'"title" should have defaulted to null');
$this->assertNull($album->title,'"title" should have defaulted to null');
}
protected function setUp()
{
\Zend\Mvc\Application::init(include'config/application.config.php');
}
}
我们测试3个问题:
所有的 Album 类的属性都是初始化为NULL?
当我们调用 exchangeArray() 方法的时候是不是 Album 属性都被设置正确了?
NULL被当作属性值在 $data 数组中是否有默认值?
如果我们运行 phpunit,我们将会知道答案都是 “YES”:
PHPUnit 3.5.15 bySebastian Bergmann.
........
Time: 0 seconds, Memory:5.50Mb
OK (8 tests, 19assertions)
接下来我们在模块的模型文件夹下创建我们的 AlbumTable 类,如下:
//module/Album/src/Album/Model/AlbumTable.php:
namespace Album\Model;
useZend\Db\TableGateway\TableGateway;
class AlbumTable
{
protected $tableGateway;
public function __construct(TableGateway$tableGateway)
{
$this->tableGateway = $tableGateway;
}
public function fetchAll()
{
$resultSet =$this->tableGateway->select();
return $resultSet;
}
public function getAlbum($id)
{
$id = (int) $id;
$rowset =$this->tableGateway->select(array('id' => $id));
$row = $rowset->current();
if (!$row) {
throw new \Exception("Couldnot find row $id");
}
return $row;
}
public function saveAlbum(Album $album)
{
$data = array(
'artist' => $album->artist,
'title' => $album->title,
);
$id = (int)$album->id;
if ($id == 0) {
$this->tableGateway->insert($data);
} else {
if ($this->getAlbum($id)) {
$this->tableGateway->update($data, array('id' => $id));
} else {
throw new \Exception('Form iddoes not exist');
}
}
}
public function deleteAlbum($id)
{
$this->tableGateway->delete(array('id' => $id));
}
}
此处我们还有很多东西准备。所以我们要在控制器中将 $tableGateway 属性设置为 TableGateway 实例。我们将要使用这一属性进行操作数据库表。
然后我们将会创建一些工具方法使得应用可以通过 Table gateway 来进行访问。fetchAll ()返回数据库中记录集为 ResultSet ,getAlbum() 将单条记录返回为 Album 对象,saveAlbum()是插入或者更新记录而 deleteAlbum 是完全删除某条数据。
使用 ServiceManager 去配置 table gateway 及注入到 Alubum表
为了能够使用如我们的Album表一样的实例,我们准备使用ServiceManager来定义如何创建它。最简单的方法是我们在Module类中创建getServiceConfig()方法,此方法会被ModuleManager自动调用然后注册到ServiceManager中。然后我们就能在控制器中需要的时候进行调用。
为了能够配置这个ServiceManager,我们即可以使得这个类成为单例或者作为一个工厂,当这个ServiceManager需要调用的时候来调用它。我们从继承getServiceConfig()这个方法到提供一个能够创建AlbumTable的工厂类。然后将这个方法添加到Module类的底部:
//module/Album/Module.php:
namespace Album;
// Add these importstatements:
use Album\Model\Album;
useAlbum\Model\AlbumTable;
useZend\Db\ResultSet\ResultSet;
useZend\Db\TableGateway\TableGateway;
class Module
{
// getAutoloaderConfig() and getConfig()methods here
// Add this method:
public function getServiceConfig()
{
return array(
'factories' => array(
'Album\Model\AlbumTable'=> function($sm) {
$tableGateway =$sm->get('AlbumTableGateway');
$table = newAlbumTable($tableGateway);
return $table;
},
'AlbumTableGateway' =>function ($sm) {
$dbAdapter =$sm->get('Zend\Db\Adapter\Adapter');
$resultSetPrototype = newResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Album());
return newTableGateway('album', $dbAdapter, null, $resultSetPrototype);
},
),
);
}
}
在调用到这个ServiceManager之前,它将会返回一个合并好的工厂类数组。Album\Model\AlbumTable的工厂类能够使用这个ServiceManager来创建一个AlbumTableGateway来调用这个AlbumTable。我们也可以认为这个ServiceManger是这个一个东西,一个AlbumTableGateway是通过获取一个Zend\Db\Adapter\Adapter(当然也是通过ServiceManager获取的)以及使用它来创建一个TableGateway对象的。而这个TableGateway 会在创建一个新的记录集的时候被告知使用一个Album对象。TableGateway类会使用原型模式来创建结果集合及实体。这就意味着当需要的时候不是实例化,而是系统会克隆之前已经实例化好的对象。看《PHP Constructor Best Practices and thePrototype Pattern》能够知道更多细节。
最后,我们需要配置好ServiceManager以便它能够知道怎么获取到一个 Zend\Db\Adapter\Adapter 。合并好的配置系统中会调用dZend\Db\Adapter\AdapterServiceFactory来使用工厂类。ZF2 的ModuleManager会将每个Module的module.config.php文件合并为一个配置文件以及config/autoload文件夹下面的文件(*.global.php及*.local.php文件)。我们将会将我们的数据库配置信息加入到global.php,同时你需要提交到你的版本控制系统。你也可以使用local.php去为你自己想要的数据库保存信息:
//config/autoload/global.php:
return array(
'db' => array(
'driver' => 'Pdo',
'dsn' =>'mysql:dbname=zf2tutorial;host=localhost',
'driver_options' => array(
PDO::MYSQL_ATTR_INIT_COMMAND =>'SET NAMES \'UTF8\''
),
),
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter'
=>'Zend\Db\Adapter\AdapterServiceFactory',
),
),
);
你需要将你数据库配置信息放到 config/autoload/local.php 文件中以便它不会出现在 git 代码仓库中(当然可以将local.php忽略掉):
//config/autoload/local.php:
return array(
'db' => array(
'username' => 'YOUR USERNAME HERE',
'password' => 'YOUR PASSWORD HERE',
),
);
测试
让我们为我们刚写的代码写一些测试。上线我们为AlbumTable创建一个测试类:
//tests/module/Album/src/Album/Model/AlbumTableTest.php:
namespace Album\Model;
usePHPUnit_Framework_TestCase;
class AlbumTableTestextends PHPUnit_Framework_TestCase
{
protected function setUp()
{
\Zend\Mvc\Application::init(include'config/application.config.php');
}
}
然后写我们的第一个测试。将这些方法加入到测试类中:
public functiontestFetchAllReturnsAllAlbums()
{
$resultSet = new ResultSet();
$mockTableGateway =$this->getMock('Zend\Db\TableGateway\TableGateway',
array('select'), array(), '', false);
$mockTableGateway->expects($this->once())
->method('select')
->with()
->will($this->returnValue($resultSet));
$albumTable = newAlbumTable($mockTableGateway);
$this->assertSame($resultSet,$albumTable->fetchAll());
}
在这个测试类中,我们来介绍一下关于Mock对象。介绍Mock对象是超出这篇教程的范围了,但是这个是一些基础的类,将会在测试中使用到。因此我们在此处会测试AlbumTable,而不是TableGateway这个类。(zend团队已经测试过TableGateway类,我们也知道它是好使的),我们只是确定下我们的AlbumTable类是和TableGateway这个类按照我们预期那般相互作用的。此外,我们也会测试AlbumTable中的fetchAll()这个方法会调用$tableGateway属性中的select()方法。如果它是可以工作的,应该会返回一个结果集对象。
最后,我们期望同样的结果集对象能够返回到调用方法中。这么个测试应该能够运行良好,所以我们将会看到我们的最后的测试方法如下:
public functiontestCanRetrieveAnAlbumByItsId()
{
$album = new Album();
$album->exchangeArray(array('id' => 123,
'artist' =>'The Military Wives',
'title' => 'In My Dreams'));
$resultSet = new ResultSet();
$resultSet->setArrayObjectPrototype(newAlbum());
$resultSet->initialize(array($album));
$mockTableGateway =$this->getMock('Zend\Db\TableGateway\TableGateway', array('select'),array(), '', false);
$mockTableGateway->expects($this->once())
->method('select')
->with(array('id' =>123))
->will($this->returnValue($resultSet));
$albumTable = newAlbumTable($mockTableGateway);
$this->assertSame($album,$albumTable->getAlbum(123));
}
public functiontestCanDeleteAnAlbumByItsId()
{
$mockTableGateway =$this->getMock('Zend\Db\TableGateway\TableGateway', array('delete'),array(), '', false);
$mockTableGateway->expects($this->once())
->method('delete')
->with(array('id' =>123));
$albumTable = newAlbumTable($mockTableGateway);
$albumTable->deleteAlbum(123);
}
public functiontestSaveAlbumWillInsertNewAlbumsIfTheyDontAlreadyHaveAnId()
{
$albumData = array('artist' => 'TheMilitary Wives', 'title' => 'In My Dreams');
$album = new Album();
$album->exchangeArray($albumData);
$mockTableGateway =$this->getMock('Zend\Db\TableGateway\TableGateway', array('insert'),array(), '', false);
$mockTableGateway->expects($this->once())
->method('insert')
->with($albumData);
$albumTable = newAlbumTable($mockTableGateway);
$albumTable->saveAlbum($album);
}
public functiontestSaveAlbumWillUpdateExistingAlbumsIfTheyAlreadyHaveAnId()
{
$albumData = array('id' => 123, 'artist'=> 'The Military Wives', 'title' => 'In My Dreams');
$album = new Album();
$album->exchangeArray($albumData);
$resultSet = new ResultSet();
$resultSet->setArrayObjectPrototype(newAlbum());
$resultSet->initialize(array($album));
$mockTableGateway =$this->getMock('Zend\Db\TableGateway\TableGateway',
array('select', 'update'), array(), '', false);
$mockTableGateway->expects($this->once())
->method('select')
->with(array('id' =>123))
->will($this->returnValue($resultSet));
$mockTableGateway->expects($this->once())
->method('update')
->with(array('artist'=> 'The Military Wives', 'title' => 'In My Dreams'),
array('id' =>123));
$albumTable = newAlbumTable($mockTableGateway);
$albumTable->saveAlbum($album);
}
public functiontestExceptionIsThrownWhenGettingNonexistentAlbum()
{
$resultSet = new ResultSet();
$resultSet->setArrayObjectPrototype(newAlbum());
$resultSet->initialize(array());
$mockTableGateway =$this->getMock('Zend\Db\TableGateway\TableGateway', array('select'),array(), '', false);
$mockTableGateway->expects($this->once())
->method('select')
->with(array('id' =>123))
->will($this->returnValue($resultSet));
$albumTable = newAlbumTable($mockTableGateway);
try
{
$albumTable->getAlbum(123);
}
catch (\Exception $e)
{
$this->assertSame('Could not findrow 123', $e->getMessage());
return;
}
$this->fail('Expected exception was notthrown');
}
让我们回顾到我们的测试。我们测试了如下内容:
很好 - 我们的AlbumTable类已经被测试过了。让我们继续进发!
返回到控制器
现在那个ServiceManager能够为我们创建一个AlbumTable实例,我们也能够在控制器中添加一个方法来获取到它。添加getAlbumTable()到AlbumController类中:
//module/Album/src/Album/Controller/AlbumController.php:
public function getAlbumTable()
{
if (!$this->albumTable) {
$sm =$this->getServiceLocator();
$this->albumTable =$sm->get('Album\Model\AlbumTable');
}
return $this->albumTable;
}
你应该添加
protected $albumTable;
到类的顶部。
我们现在能够在我们需要和模型交互的时候调用控制器中的 getAlbumTable() 方法。现在让我们通过写个测试来确定它是能够工作的。
添加个测试到你的 AlbumControllerTest 类中:
public functiontestGetAlbumTableReturnsAnInstanceOfAlbumTable()
{
$this->assertInstanceOf('Album\Model\AlbumTable',$this->controller->getAlbumTable());
}
如果在Module.php文件中,servicelocator(服务定位器?)配置的正确,当我们调用getAlbumTable()方法的时候是可以得到\Model\AlbumTable实例的。
列出唱片封面:
为了能够列出唱片封面,我们需要通过模型来调用这些东西到显示层中。为了达到这一目的,在AlbumController的indexAction()中写一些代码。修改过的代码如下:
//module/Album/src/Album/Controller/AlbumController.php:
// ...
public function indexAction()
{
return new ViewModel(array(
'albums' =>$this->getAlbumTable()->fetchAll(),
));
}
// ...
使用ZF2,为了在显示层中设置变量,我们会返回一个显示Model实例。而显示层(view script)会自动调用它。显示model对象也会让我们改变使用的显示脚本,但是默认的是使用{controller name}/{actionname}。我们现在在 index.phtml显示脚本中的代码如下:
//module/Album/view/album/album/index.phtml:
$title = 'My albums';
$this->headTitle($title);
?>
array('action'=>'edit', 'id'=> $album->id));?>">Edit
array('action'=>'delete', 'id'=> $album->id));?>">Delete
第一件事情是我们要为这个页面设置title(在布局(layout)中使用)然后为标签部分使用Title()显示小工具设置title,它会出现在浏览器的title栏。然后我们会创建一个链接去添加一个新的唱片封面。
url显示层小工具是由ZF2提供,被用来为我们创建各种链接的。第一个参数是我们希望用作url结构的路由名称,第二个参数是一些会被替换掉的变量参数。在这个例子中我们使用“album” 路由以及两个变量:action及id。
通过控制器我们会循环我们的$albums 变量。ZF2的显示系统会自动将这个变量循环出现到显示脚本的中,所以我们不在需要像ZF1那般使用 $this-> 来调用它。当然你如果喜欢这么做也可以做。
然后我们创建一个表来显示每个唱片封面的标题及艺术家们,以及编辑及删除链接。一个标准的循环foreach:循环唱片封面列表,然后我们通过使用endforeach来结束。这么我们也很容易匹配标签(检查代码)。再者说道,url()这么个显示小工具被用来创建编辑及删除链接。
注意:
我们经常使用 escapeHtml() 方法来过滤数据以防止XSS攻击。
如果你打开http://zf2-tutorial.localhost/album,你应该能看到如下截图: