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

Базы данных править

Мы уже создали модуль 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.

Будем использовать сущности. Создайте класс «Album» в папке «Model» (на одном уровне с Controller).

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

Наш объект сущности Album представляет собой обычный класс PHP. Для согласованной работы с Zend\Db\TableGateway\AbstractTableGateway мы создали метод exchangeArray(), который просто копирует данные, пришедшие в виде массива в свойства сущности. Также добавим входной фильтр(inputfilter) для дальнейшего использования с формами(form).

Далее расширим класс Zend\Db\TableGateway\AbstractTableGateway создав собственный класс AlbumTable в той же директории(Model):

// module/Album/src/Album/Model/AlbumTable.php:
namespace Album\Model;

use Zend\Db\Adapter\Adapter;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\TableGateway\AbstractTableGateway;

class AlbumTable extends AbstractTableGateway
{
    protected $table ='album';
 
    public function __construct(Adapter $adapter)
    {
        $this->adapter = $adapter;
 
        $this->resultSetPrototype = new ResultSet();
        $this->resultSetPrototype->setArrayObjectPrototype(new Album());

        $this->initialize();
    }

    public function fetchAll()
    {
        $resultSet = $this->select();
        return $resultSet;
    }

    public function getAlbum($id)
    {
        $id  = (int) $id; 
 
        $rowset = $this->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->insert($data);
        } elseif ($this->getAlbum($id)) {
            $this->update(
                $data,
                array(
                    'id' => $id,
                )
            );
        } else {
            throw new Exception('Form id does not exist');
        }
    }

    public function deleteAlbum($id)
    {
        $this->delete(array(
            'id' => $id,
        ));
    }
}


Давайте разберемся с кодом. Сначала мы устанавливаем защищенное свойство(переменная) $table, в которую заносим имя таблицы «album». Потом создаем конструктор(__construct), который принимает адаптер БД(Adapter $adapter) и делает доступным для использования его в нашем классе. Далее ставим в известность шлюз, что при каждом новом создании строки нужно использовать объект Album. Класс TableGateway использует паттерн prototype для создания выходных данных и сущностей. Это означает, что при необходимости создания экземпляра берется клон ранее созданного экземпляра класса. Более подробно можете почитать тут: PHP Constructor Best Practices and the Prototype Pattern

Далее мы создали еще несколько дополнительных методов для связи с таблицей в БД. fetchAll() - вытаскивает все строки из таблицы и возвращает их как ResultSet, getAlbum() – возвращает одну строку как объект Album, saveAlbum() – создает/обновляет строку в БД, deleteAlbum() – удаляет строку.

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

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

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

// module/Album/Module.php:
namespace Album;

// Add this import statement:
use AlbumModel\AlbumTable;

class Module
{
    // getAutoloaderConfig() and getConfig() methods here

    // Add this method:
    public function getServiceConfig()
    {
        return array(
            'factories' => array(
                'Album\Model\AlbumTable' =>  function($sm) {
                    $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
                    $table     = new AlbumTable($dbAdapter);
                    return $table;
                },
            ),
        );
    }
}

Этот метод возвращает массив фабрик, которые в итоге объединяются в ModuleManager перед отправкой в ServiceManager. Теперь необходимо настроить ServiceManager так, что б он знал как получить адаптер Zend\Db\Adapter\Adapter. Это обеспечивается вызовом фабрики Zend\Db\Adapter\AdapterServiceFactory, которую мы можем настроить в конфигурационном файле приложения.

В Zend Framework 2 ModuleManager сначала забирает все настройки с файлов конфигурации модулей(module.config.php), потом с cofig/autoload(сначала с *.global.php, потом с *.local.php, и потом остальных которые в этой директории ). Мы добавим настройки для подключения к БД в global.php. Так же можете использовать файл local.php для хранения настроек (за пределами VCS) если необходимо.

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

Теперь, когда 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). Построим список альбомов при вызове действия index.

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

Для вывода списка альбомов необходимо извлечь данные из модели и передать в шаблон вида. Для этого допишем следующий код в действие 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);
?>
<?php echo $this->escapeHtml($title); ?>
<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> </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 то увидите нечто подобное: