Содержание


Работа с базой данных и моделями


Database and models



 Базы данных

Мы уже создали модуль Album, необходимые контроллеры, методы и шаблоны. Пришло время рассмотреть модели(model). В классической теории баз данных, модель данных есть формальная теория представления и обработки данных в системе управления базами данных (СУБД), которая включает, по меньшей мере, три аспекта:

1) аспект структуры: методы описания типов и логических структур данных в базе данных;

2) аспект манипуляции: методы манипулирования данными;

3) аспект целостности: методы описания и поддержки целостности базы данных.

Мы будем использовать класс ядра фреймворка Zend\Db\Gateway\TableGateway, который используется для таких операций с БД как:find(найти), insert(вставка), update(обновление) and delete(удаление) строк.

Используем MySQL через PHP драйвер PDO. Создайте БД zf2tutorial, и создайте таблицу «album» . Для этого можно использовать следующий код:

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');


Теперь, когда у нас есть БД, таблица с данными можем приступить к созданию простой модели.

Модель

Zend Framework 2 не предоставляет компонент Zend\Model, поэтому разработчику нужно самому решать, как он организует свою бизнес логику работы с БД. Существует большое количество различных решений этой проблемы. Одним из решений является использование сущностей и мапера(mapper) для работы с БД. Другим выходом является использование ORM(технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных») таких как Doctrine или Propel.


В этом руководстве мы создадим очень простую модель путем создания класса AlbumTable, использующего класс Zend/Db/TableGateway, в свою очередь в котором каждый объект альбома – это объект Album (именуемый сущностью). Это пример реализации шаблона проектирования под названием Шлюз к данным таблицы (TableDataGeteway), который обеспечивает с данными в таблице БД. Однако, следует помнить, что этот шаблон может стать ограниченным в больших системах. На ряду с этим может возникнуть желание поместить доступ к базе данных в метод контроллера, как это представлено в Zend/Db/TableGatewayAbstractTableGateway. Не делайте этого!

Для начала создайте файл Album.php в каталоге module/Album/src/Album/Model:

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

Наш объект сущности Album представляет собой обычный класс PHP. Для согласованной работы с ZendDbTableGatewayAbstractTableGateway мы создали метод exchangeArray(), который просто копирует данные, пришедшие в виде массива в свойства сущности. Фильтр для нашей формы мы добавим немного позже.

 

Для начала нужно проверить, работает ли модель Album так, как этого мы от нее ожидаем? Для уверенности давайте напишем несколько тестов. Создайте файл AlbumTest.phpв каталоге module/Album/test/AlbumTest/Model:

namespace Album\TestModel;
 
use Album\Model\Album;
use PHPUnit_Framework_TestCase;
 
class AlbumTest extends PHPUnit_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 function testExchangeArraySetsPropertiesCorrectly()
    {
        $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, '"id" was not set correctly');
        $this->assertSame($data['title'], $album->title, '"title" was not set correctly');
    }
 
    public function testExchangeArraySetsPropertiesToNullIfKeysAreNotPresent()
    {
        $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, '"id" should have defaulted to null');
        $this->assertNull($album->title, '"title" should have defaulted to null');
    }
}

Мы сделаем 3 проверки:

1.    Все ли свойства альбома изначально равны NULL?

2.    Корректны ли значения свойств в момент вызова метода exchangeArray()?

3.    Будет ли значение по умолчанию NULL использоваться для свойств, ключей которого нет в массиве $data?

Если мы запустим PHPUnit снова, мы увидим, что ответ на все три вопроса положительный:

PHPUnit 3.5.15 by Sebastian Bergmann.
 
........
 
Time: 0 seconds, Memory: 5.50Mb
 
OK (8 tests, 19 assertions)
 

На следующем шаге создайте файл AlbumTable.php  вкаталоге module/Album/src/Album/Model:

<?php
namespace Album\Model;
 
use Zend\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("Could not 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 id does not exist');
            }
        }
    }
 
    public function deleteAlbum($id)
    {
        $this->tableGateway->delete(array('id' => $id));
    }
}

Давайте детальнее разберем вышеприведенный код. Сначала, мы прописали protectedсвойство $tableGateway, которое попадает в конструктор класса. Это будет использоваться для выполнения операций на таблицей базы данных для нашего альбома.

Затем мы создаем некоторые вспомогательные методы, которые наше приложение будет использовать для взаимодействия с таблицей. FetchAll() возвращает из БД все альбомы построчно, getAlbum() возвращает одну запись в виде объекта Album, saveAlbum() или создает новую запись в БД или обновляет уже существующую запись, и deleteAlbum() удаляет запись полностью. Код для каждого из этих методов, надеемся, Вам понятен.

 


Использование ServiceManager для настройки доступа к базе данных и инъекции в контроллер

 

Для использования экземпляра класса AlbumTable воспользуемся ServiceManager. Самый простой способ данной реализации это создание метода getServiceConfig() в файле Module.php, так как ModuleManager будет вызывать его автоматически и передавать в  ServiceManager. И у нас будет возможность вызвать его с любого нашего класса.

 

Для настройки ServiceManager необходимо указать имя класса, которое будет создано, либо фабрику, которая создаст необходимый объект при вызове. Мы создадим фабрику для AlbumTable. Добавьте следующий код в конец класса Module:

<?php
namespace Album;
 
// Add these import statements:
use Album\Model\Album;
use Album\Model\AlbumTable;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\TableGateway\TableGateway;
 
class Module
{
    // getAutoloaderConfig() and getConfig() methods here
 
    // Add this method:
    public functiocolor:#66cc66color:#000000;font-weight:bold n getServiceConfig()
    {
        return array(
            'factories' => array(
                'AlbumModelAlbumTable' =>  function($sm) {
                    $tableGateway = $sm->get('AlbumTableGateway');
                    $table = new AlbumTable($tableGateway);
                    return $table;
                },
                'AlbumTableGateway' => function ($sm) {
                    $dbAdapter = $sm->get('ZendDbAdapterAdapter');
                    $resultSetPrototype = new ResultSet();
                    $resultSetPrototype->setArrayObjectPrototype(new Album());
                    return new TableGateway('album', $dbAdapter, null, $resultSetPrototype);
                },
            ),
        );
    }
}

Этот метод возвращает массив фабрик, которые объединены с помощью ModuleManager прежде чем попасть в ServiceManager. Фабрика для Album\Model\AlbumTable использует ServiceManager для создания AlbumTableGateway для перехода к AlbumTable.

AlbumTableGateway создается путем получения ZendDbAdapterAdapter (также из ServiceManager) и использует его для создания объекта TableGateway. TableGateway использует объект Album всегда, когда он создает новый результат запроса. Класс TableGateway использует паттерн prototype для создания выходных данных и сущностей. Это означает, что при необходимости создания экземпляра берется клон ранее созданного экземпляра класса. Более подробно можете почитать тут: PHP Constructor Best Practices and the Prototype Pattern.

Наконец, нам нужно сконфигурировать ServiceManager так, чтобы он знал, как получить Zend\Db\Adapter\Adapter. Это делается с помощью фабрики Zend\Db\Adapter\AdapterServiceFactory, которую можно настроить путем слияния некоторых настроек системы. ModuleManager сначала объединяет все настройки каждого файла модуля module.config.phpи потом объединяет в файлы в config/autoload (*.global.phpи *.local.php). Мы добавим информацию о настройках базы данных в файл global.php, который необходимо  должны добавить под управление системы контроля версий. Вы также можете использовать local.php (за пределами системы контроля версий) для хранения учетных записей базы данных. Отредактируйте файл в соответствии с приведенным ниже примером:

<?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(
            'ZendDbAdapterAdapter'
                    => 'Zend\Db\Adapter\AdapterServiceFactory',
        ),
    ),
);

Вы должны поместить Ваши учетные записи в файл config/autoload/local.php, но при этом он не должен находится в Git репозитории.

<?php
return array(
    'db' => array(
        'username' => 'YOUR USERNAME HERE',
        'password' => 'YOUR PASSWORD HERE',
    ),
);

Тестирование


Давайте напишем несколько тестов для выше приведенного кода. Для начала, создайте тестовый класс для AlbumTable. Создайтефайл AlbumTableTest.php в module/Album/test/AlbumTest/Model.

<?php
namespace AlbumTestModel;
 
use Album\Model\AlbumTable;
use Album\Model\Album;
use Zend\Db\ResultSet\ResultSet;
use PHPUnit_Framework_TestCase;
 
class AlbumTableTest extends PHPUnit_Framework_TestCase
{
    public function testFetchAllReturnsAllAlbums()
    {
        $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 = new AlbumTable($mockTableGateway);
 
        $this->assertSame($resultSet, $albumTable->fetchAll());
    }
}

В этом тесте мы вводим понятие Mock объектов. Детальное рассмотрение понятия Mock объектов выходит за пределы этого руководства. Вкратце, Mock объект – это объект, который занимает место другого объекта и ведет себя предопределенным способом. Поскольку мы тестируем AlbumTable, а  не класс TableGateway (команда Zend уже протестировала класс TableGateway, и мы знаем, что он работает), то мы просто хотим убедиться, что класс AlbumTable взаимодействует с классом TableGateway так, как мы ожидаем. Сначала делается проверка, чтобы убедиться, что метод FetchAll() вызывает метод select() свойства $tableGateway без параметров. Если это происходит, то нужно возвращать объект ResultSet. И в конце, мы ожидаем, что этот же объект ResultSet будет возвращен вызывающему методу. Этот тест должен работать нормально, так что теперь мы можем добавить остальные тесты:

public function testCanRetrieveAnAlbumByItsId()
{
    $album = new Album();
    $album->exchangeArray(array('id'     => 123,
                                'artist' => 'The Military Wives',
                                'title'  => 'In My Dreams'));
 
    $resultSet = new ResultSet();
    $resultSet->setArrayObjectPrototype(new Album());
    $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 = new AlbumTable($mockTableGateway);
 
    $this->assertSame($album, $albumTable->getAlbum(123));
}
 
public function testCanDeleteAnAlbumByItsId()
{
    $mockTableGateway = $this->getMock('Zend\Db\TableGateway\TableGateway', array('delete'), array(), '', false);
    $mockTableGateway->expects($this->once())
                     ->method('delete')
                     ->with(array('id' => 123));
 
    $albumTable = new AlbumTable($mockTableGateway);
    $albumTable->deleteAlbum(123);
}
 
public function testSaveAlbumWillInsertNewAlbumsIfTheyDontAlreadyHaveAnId()
{
    $albumData = array('artist' => 'The Military 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 = new AlbumTable($mockTableGateway);
    $albumTable->saveAlbum($album);
}
 
public function testSaveAlbumWillUpdateExistingAlbumsIfTheyAlreadyHaveAnId()
{
    $albumData = array('id' => 123, 'artist' => 'The Military Wives', 'title' => 'In My Dreams');
    $album     = new Album();
    $album->exchangeArray($albumData);
 
    $resultSet = new ResultSet();
    $resultSet->setArrayObjectPrototype(new Album());
    $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 = new AlbumTable($mockTableGateway);
    $albumTable->saveAlbum($album);
}
 
public function testExceptionIsThrownWhenGettingNonexistentAlbum()
{
    $resultSet = new ResultSet();
    $resultSet->setArrayObjectPrototype(new Album());
    $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 = new AlbumTable($mockTableGateway);
 
    try
    {
        $albumTable->getAlbum(123);
    }
    catch (Exception $e)
    {
        $this->assertSame('Could not find row 123', $e->getMessage());
        return;
    }
 
    $this->fail('Expected exception was not thrown');
}

Давайте разберем наши тесты. Мы проверяем, что:

1)    Мы можем получить конкретный альбом по его идентификатору.

2)    Мы можем удалить альбомы.

3)    Мы можем сохранить новый альбом.

4)    Мы можем обновить существующие альбомы.

5)    Мы сталкиваемся с исключением, если мы пытаемся получить альбом, который не существует.

 

Великолепно - наш класс 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() доступен с любого места нашего класса для взаимодействия с моделью(model). Давайте убедимся в работоспособности этого кода с помощью написания теста.

 

Добавьте этот тест в класс AlbumControllerTest:

public function testGetAlbumTableReturnsAnInstanceOfAlbumTable()
{
    $this->assertInstanceOf('Album\Model\AlbumTable', $this->controller->getAlbumTable());
}

Если сервис локатор был правильно настроен в module.php, то мы должны получить экземпляр AlbumModelAlbumTable при вызове getAlbumTable().

 

Список альбомов

 

Для вывода списка альбомов необходимо извлечь данные из модели и передать в шаблон вида. Для этого допишем следующий код в действие indexAction контроллера AlbumController:

// module/Album/src/Album/Controller/AlbumController.php:
// ...
    public function indexAction()
    {
        return new ViewModel(array(
            'albums' => $this->getAlbumTable()->fetchAll(),
        ));
    }
// ...

В ZendFramework 2 для передачи переменных в шаблон вида необходимо вернуть экземпляр ViewModel, где первый параметр будет массив с данными, переданными из контроллера. Тогда они автоматически попадут в шаблон вида. Объект ViewModel позволяет изменить скрипт вида, куда будет передана информация, но по умолчанию передается в {имя контроллера(controllername)}/{имя действия(actionname)}. Теперь приступим к заполнению шаблона вида(скрипт вида, view script):

<?php
// module/Album/view/album/album/index.phtml:
 
$title = 'My albums';
$this->headTitle($title);
?>
<h1><?php echo $this->escapeHtml($title); ?></h1>
<p>
    <a href="<?php echo $this->url('album', array('action'=>'add'));?>">Add new album</a>
</p>
 
<table class="table">
<tr>
    <th>Title</th>
    <th>Artist</th>
    <th>&nbsp;</th>
</tr>
<?php foreach ($albums as $album) : ?>
<tr>
    <td><?php echo $this->escapeHtml($album->title);?></td>
    <td><?php echo $this->escapeHtml($album->artist);?></td>
    <td>
        <a href="<?php echo $this->url('album',
            array('action'=>'edit', 'id' => $album->id));?>">Edit</a>
        <a href="<?php echo $this->url('album',
            array('action'=>'delete', 'id' => $album->id));?>">Delete</a>
    </td>
</tr>
<?php endforeach; ?>
</table>

Для начала установим заголовки страниц в теге <head>  используя метод помощника вида headTitle(). Эти заголовки будут показаны в строке заголовка браузера. Потом добавим ссылку для добавления нового альбома.

Помощник вида url() используется для создания необходимых нам ссылок. Первый параметр указывает на имя роута(route), который мы хотим использовать для генерации ссылки, второй – является массивом, содержащим переменные для подстановки в заполнитель(placeholder). В данном случае имя роута «album», а переменные для заполнители будут: «action» и «id».

Делаем перебор переменной $albums, которую отправили из действия в вид. В Zend Framework 2 нет необходимости добавлять к переменным префикс $this, так как об этом заботится сам фреймворк, и гарантировано получаем в шаблоне вида все переменные, отправленные из соответствующего контроллера. Хотя можно и добавить, если это необходимо.

Создаем таблицу для вывода названия альбома(title) и имени артиста(artist), а так же добавляем ссылки на редактирование и удаление альбома. Оператор цикла foreach используется для перебора всех значений. В данном примере мы используем альтернативную его форму с двоеточием и endforeach потому, что так визуально легче написать правильный код и не нужно искать совпадающие скобки для открытия/закрытия…

Важно: В примере мы везде используем помощник вида escapeHtml() для обеспечения безопасности и защиты от XSSатак.

Теперь, если Вы перейдете по адресу http://zf2-tutorial.localhost/album то увидите нечто подобное:



Автор статьи: DuB