Pull to refresh

Введение в Zend Framework (продолжение)

Reading time12 min
Views17K
Original author: Rob Allen
Продолжаем рассказ о Zend Framework. В первой части статьи была описана концепция программной архитектуры MVC, рассмотрена структура типового веб-приложения, базирующегося на Zend Framework и выполнена демонстрационная реализация контроллера и вида на его основе. Во второй части будет раскрыта тема модели и приведен пример взаимодействия приложения с базой данных.

Автор: Роб Ален, akrabat.com
Оригинал: akrabat.com/zend-framework-tutorial
Перевод: Александр Мусаев, musayev.com

База данных


Теперь, когда управляющая часть нашего приложения и код визуализации разделены, пришло время заняться моделью. Запомните, что модель — базовая часть приложения, которая реализует его основные функции. Следовательно, в нашем случае модель выполняет работу с базой данных. Мы используем класс Zend Framework Zend_Db_Table, предназначенный для поиска, вставки, обновления и удаления записей в таблице базы данных.

Настройка


Для использования Zend_Db_Table, понадобится сообщить ему, к какой базе данных, под каким именем пользователя и с каким паролем он будет обращаться. Принимая во внимание, что эту информацию предпочтительно не забивать в код, воспользуемся конфигурационным файлом для ее хранения.

Zend Framework предоставляет для этой цели класс Zend_Config, обеспечивающий гибкий объектно-ориентированный доступ к конфигурационным файлам в форматах INI и XML. Остановим свой выбор на INI-файле:

zf-tutorial/application/config.ini:

[general]
db.adapter = PDO_MYSQL
db.config.host = localhost
db.config.username = rob
db.config.password = 123456
db.config.dbname = zftest

(Безусловно, вам понадобится использовать свои собственные параметры доступа к БД, а не приведенные в примере.)

Использовать Zend_Config будет очень просто:

$config = new Zend_Config_Ini('config.ini', 'section');

В данном случае, Zend_Config_Ini загружает одну секцию из INI-файла (таким же образом при необходимости можно загрузить любую другую секцию). Возможность именования секций реализована для того, чтобы лишние данные без нужды не загружались. Zend_Config_Ini использует точку в именах параметров в качестве иерархического разделителя, благодаря чему можно группировать родственные параметры. В нашем файле config.ini, параметры host, username, password и dbname будут сгруппированы в объекте $config->db->config.

Мы будем загружать конфигурационный файл из файла начальной загрузки (index.php):

zf-tutorial/index.php:

...
Zend_Loader::loadClass('Zend_Controller_Front');
Zend_Loader::loadClass('Zend_Config_Ini');
Zend_Loader::loadClass('Zend_Registry');

// load configuration
$config = new Zend_Config_Ini('./application/config.ini', 'general');
$registry = Zend_Registry::getInstance();
$registry->set('config', $config);

// setup controller
...

После загрузки необходимых для работы классов (Zend_Config_Ini и Zend_Registry), в объект $config загружается секция конфигурационного файла application/config.ini под именем general. Далее объект $config включается в реестр, что обеспечивая доступ к нему из всего приложения.

Замечание: В данном примере нет реальной потребности хранить $config в реестре. Это сделано в качестве примера работы «настоящего» приложения, в конфигурационном файле которого вам, вероятно, придется хранить нечто большее, чем параметры доступа к БД. При использовании реестра, обратите так же внимание, что данные в нем доступны на глобальном уровне, и что при неосторожном использовании это может стать причиной потенциальных конфликтных ситуаций внутри приложения.

Использование Zend_Db_Table

Для того, чтобы использовать класс Zend_Db_Table, нам понадобится передать в него параметра доступа к базе данных, которые были только что загружены. Для этого необходимо создать объект Zend_Db и зарегистрировать его функцией Zend_Db_Table::setDefaultAdapter(). Ниже приведен соответствующий фрагмент кода в файле первичной загрузки:

zf-tutorial/index.php:

...
Zend_Loader::loadClass('Zend_Controller_Front');
Zend_Loader::loadClass('Zend_Config_Ini');
Zend_Loader::loadClass('Zend_Registry');
<b>Zend_Loader::loadClass('Zend_Db');
Zend_Loader::loadClass('Zend_Db_Table');</b>

// load configuration
$config = new Zend_Config_Ini('./application/config.ini', 'general');
$registry = Zend_Registry::getInstance();
$registry->set('config', $config);

<b>// setup database
$db = Zend_Db::factory($config->db->adapter,
$config->db->config->toArray());
Zend_Db_Table::setDefaultAdapter($db);</b>

// setup controller
...

Создание таблицы

Мы будем использовать базу данных MySQL, поэтому SQL запрос на создание таблицы будет выглядеть так:

CREATE TABLE album (
    id int(11) NOT NULL auto_increment,
    artist varchar(100) NOT NULL,
    title varchar(100) NOT NULL,
    PRIMARY KEY (id)
);

Этот запрос можно выполнить через любой MySQL-клиент, например, phpMyAdmin или стандартную консольную утилиту.

Добавление тестовой записи

Добавим несколько тестовых записей в таблицу для проверки функциональности главной страницы, на которой они в последствии должны отображаться.

INSERT INTO album (artist, title)
VALUES 
    ('James Morrison', 'Undiscovered'),
    ('Snow Patrol', 'Eyes Open');

Модель


Zend_Db_Table — абстрактный класс, поэтому на его основе необходимо создать специализированный на нашей задаче класс-наследник. Имя нового класса не имеет принципиального значения, но такие классы стоит называть аналогично соответствующим им таблицам БД (это повысит уровень самодокументируемости кода). Таким образом, для нашей таблицы album имя класса будет Album.

Для того, чтобы передать в Zend_Db_Table имя таблицы, которой он будет управлять, необходимо присвоить это значение защищенному свойству класса $_name. Стоит отметить, что в классе Zend_Db_Table по-умолчанию всегда используется ключевое автоинкрементное поле таблицы с именем id. При необходимости значение имени этого поля так же можно менять.

Класс Album будет храниться в директории моделей.

zf-tutorial/application/models/Album.php:

<?php

class Album extends Zend_Db_Table
{
    protected $_name = 'album';
}

Ничего сложного, не так ли? Учитывая, что наши потребности в данном случае совсем невелики, базовой функциональности Zend_Db_Table хватит, чтобы полностью их удовлетворить. Если же в ваших задачах понадобится реализовать более сложные операции взаимодействия с базой данных, то класс модели — это именно то место, куда стоит поместить соответствующий код.

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


Теперь, когда мы сконфигурировали базу данных, можно приступить к выполнению основной задачи приложения и отобразить несколько записей. Это должно быть реализовано в классе IndexController. Каждая функция-действие внутри IndexController взаимодействует с таблицей album через класс Album. Поэтому имеет смысл загрузить его при инициализации контролера внутри функции init().

zf-tutorial/application/controllers/IndexController.php:

...
function init()
{
    $this->view->baseUrl = $this->_request->getBaseUrl();
    Zend_Loader::loadClass('Album');
}
...

Замечание: В данном фрагменте кода приведен пример использования метода Zend_Loader::loadClass() для загрузки нестандартных классов. Это срабатывает, т. к. директория моделей (в которой хранится подгружаемый класс Albums) была добавлена нами в include_path.

Создадим список альбомов из базы с помощью indexAction():

zf-tutorial/application/controllers/IndexController.php:

...
function indexAction()
{
    $this->view->title = "My Albums";
    $album = new Album();
    $this->view->albums = $album->fetchAll();
}
...

Функция fetchAll() возвращает объект Zend_Db_Table_Rowset, который позволит нам сформировать список из всех записей в шаблонном файле вида:
zf-tutorial/application/views/scripts/index/index.phtml:

<?php echo $this->render('header.phtml'); ?>
<h1><?php echo $this->escape($this->title); ?></h1>
<p><a href="<?php echo $this->baseUrl; ?>/index/add">Add new album</a></p>
<table>
<tr>
<th>Title</th>
<th>Artist</th>
<th> </th>
</tr>
<?php foreach($this->albums as $album) : ?>
<tr>
<td><?php echo $this->escape($album->title);?></td>
<td><?php echo $this->escape($album->artist);?></td>
<td>
<a href="<?php echo $this->baseUrl; ?>/index/edit/id/<?php
echo $album->id;?>">Edit</a>
<a href="<?php echo $this->baseUrl; ?>/index/delete/id/<?php
echo $album->id;?>">Delete</a>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php echo $this->render('footer.phtml'); ?>

По адресу localhost/zf-tutorial теперь должен отображаться список из двух наших альбомов.

Добавление нового альбома


Теперь перейдем к функции добавления нового диска в базу. Эта задача состоит из двух частей:
  • отображение формы, через которую пользователь будет вводить данные;
  • добавление принятых из формы данных в базу.

Реализуем перечисленные операции в функции-действии addAction():

zf-tutorial/application/controllers/IndexController.php:

...
function addAction()
{
    $this->view->title = "Add New Album";

    if ($this->_request->isPost()) {
        Zend_Loader::loadClass('Zend_Filter_StripTags');
        $filter = new Zend_Filter_StripTags();
        $artist = $filter->filter($this->_request->getPost('artist'));
        $artist = trim($artist);
        $title = trim($filter->filter($this->_request->getPost('title')));

        if ($artist != '' && $title != '') {
            $data = array(
                'artist' => $artist,
                'title' => $title,
            );
            $album = new Album();
            $album->insert($data);
            $this->_            redirect('/');
            return;
        }
    }

    // set up an "empty" album
    $this->view->album = new stdClass();
    $this->view->album->id = null;
    $this->view->album->artist = '';
    $this->view->album->title = '';

    // additional view fields required by form
    $this->view->action = 'add';
    $this->view->buttonText = 'Add';</b>
}
...

Обратите внимание, как в начале функции было определено, имела ли место пересылка данных их формы. Если данные из формы были переданы, мы извлекаем значения artist и title, и обрабатываем их фильтром HTML тагов Zend_Filter_StripTags. Далее, если строки имеют непустое значение, мы используем класс Album() для добавления новой записи в таблицу БД. После добавления альбома, пользователь переадресуется обратно на главную страницу методом контроллера _redirect().

Последнее, что понадобится сделать, — подготовить HTML-форму для шаблона вида. Забегая вперед, можно отметить, что форма редактирования записей будет выглядеть идентично форме для их добавления. Поэтому мы используем общий шаблонный файл (_form.html), который будет использован в add.phtml и edit.phtml:

Шаблон страницы добавления нового альбома:

zf-tutorial/application/views/scripts/index/add.phtml:

<?php echo $this->render('header.phtml'); ?>
<h1><?php echo $this->escape($this->title); ?></h1>
<?php echo $this->render('index/_form.phtml'); ?>
<?php echo $this->render('footer.phtml'); ?>

zf-tutorial/application/views/scripts/index/_form.phtml:

<form action="<?php echo $this->baseUrl ?>/index/<?php
echo $this->action; ?>" method="post">
<div>
<label for="artist">Artist</label>
<input type="text" name="artist"
value="<?php echo $this->escape(trim($this->album->artist));?>"/>
</div>
<div>
<label for="title">Title</label>
<input type="text" name="title"
value="<?php echo $this->escape($this->album->title);?>"/>
</div>
<div id="formbutton">
<input type="hidden" name="id" value="<?php echo $this->album->id; ?>" />
<input type="submit" name="add"
value="<?php echo $this->escape($this->buttonText); ?>" />
</div>
</form>

Получился достаточно несложный код. Учитывая, что мы предполагаем применять _form.phtml еще и при редактировании записей, используем переменную $this->action, вместо того, чтобы жестко задавать имя необходимого действия. Таким же образом, через переменную задается надпись на кнопке передачи данных из формы.

Редактирование альбома


Редактирование альбома во многом идентично добавлению новой записи, поэтому и код получается похожим:

zf-tutorial/application/controllers/IndexController.php:

...
function editAction()
{
    $this->view->title = "Edit Album";
    $album = new Album();

    if ($this->_request->isPost()) {
        Zend_Loader::loadClass('Zend_Filter_StripTags');
        $filter = new Zend_Filter_StripTags();
        $id = (int)$this->_request->getPost('id');
        $artist = $filter->filter($this->_request->getPost('artist'));
        $artist = trim($artist);
        $title = trim($filter->filter($this->_request->getPost('title')));

        if ($id !== false) {
            if ($artist != '' && $title != '') {

                $data = array(
                    'artist' => $artist,
                    'title' => $title,
                );

                $where = 'id = ' . $id;
                $album->update($data, $where);
                $this->_redirect('/');
                return;
            } else {
                $this->view->album = $album->fetchRow('id='.$id);
            }
        }
    } else {
        // album id should be $params['id']
        $id = (int)$this->_request->getParam('id', 0);

        if ($id > 0) {
            $this->view->album = $album->fetchRow('id='.$id);
        }
    }

    // additional view fields required by form
    $this->view->action = 'edit';
    $this->view->buttonText = 'Update';
}
...

Стоит отметить, что в тех случаях, когда не была выполнена передача данных в скрипт из формы, мы можем получить параметр id из свойства params объекта Request, с помощью метода getParam().

Код шаблона приведен ниже:

zf-tutorial/application/views/scripts/index/edit.phtml:

<?php echo $this->render('header.phtml'); ?>
<h1><?php echo $this->escape($this->title); ?></h1>
<?php echo $this->render('index/_form.phtml'); ?>
<?php echo $this->render('footer.phtml'); ?>

Рефакторинг


Безусловно, от вашего внимания не могло ускользнуть, что addAction() и editAction() очень похожи, а шаблоны добавления и редактировании записей вообще идентичны. Логично предположить необходимость рефакторинга кода. Решите эту задачу самостоятельно, в качестве дополнительного практического упражнения.

Удаление альбома


Для того, чтобы завершить разработку приложения, понадобится добавить функцию удаления записей. У нас предусмотрены ссылки для удаления альбомов напротив каждого элемента в списке. Первое приходящее на ум решение — удалять записи сразу при клике по одной из этих ссылок. Но это неправильный подход. Вспомните, что согласно спецификации HTTP, не следует выполнять необратимых действий с помощью метода GET. В таких случаях рекомендуется применять POST. Примером того может быть работа Google Accelerator.

Мы должны запрашивать подтверждение перед удалением записей и удалять их только после того, как оно будет получено. Код, реализующий эти действия, в некоторой степени похож на уже существующие в нашем приложении функции-действия:

zf-tutorial/application/controllers/IndexController.php:

...
function deleteAction()
{
    $this->view->title = "Delete Album";
    $album = new Album();

    if ($this->_request->isPost()) {
        Zend_Loader::loadClass('Zend_Filter_Alpha');
        $filter = new Zend_Filter_Alpha();
        $id = (int)$this->_request->getPost('id');
        $del = $filter->filter($this->_request->getPost('del'));

        if ($del == 'Yes' && $id > 0) {
            $where = 'id = ' . $id;
            $rows_affected = $album->delete($where);
        }
    } else {
        $id = (int)$this->_request->getParam('id');

        if ($id > 0) {
            // only render if we have an id and can find the album.
            $this->view->album = $album->fetchRow('id='.$id);

            if ($this->view->album->id > 0) {
                // render template automatically
                return;
            }
        }
    }

    // redirect back to the album list unless we have rendered the view
    $this->_redirect('/');
}
...

Использован тот же способ определения метода обращения к скрипту и выбора требуемой операции (удаления записи или выдачи формы подтверждения). Точно так же, как в случае с добавлением и редактированием, удаление выполняется через Zend_Db_Table методом delete(). В конце функции выполняется перенаправление пользователя на страницу со списком альбомов. Таким образом, если одна из проверок корректности параметров не пройдена, происходит возврат на исходную страницу без помощи многократного обращения к _redirect() внутри функции.

Шаблон представляет собой простую форму:
zf-tutorial/application/views/scripts/index/delete.phtml:

<?php echo $this->render('header.phtml'); ?>
<h1><?php echo $this->escape($this->title); ?></h1>
<b><?php if ($this->album) :?>
<form action="<?php echo $this->baseUrl ?>/index/delete" method="post">
<p>Are you sure that you want to delete
    '<?php echo $this->escape($this->album->title); ?>' by
    '<?php echo $this->escape($this->album->artist); ?>'?
</p>
<div>
    <input type="hidden" name="id" 
        value="<?php echo $this->album->id; ?>" />
    <input type="submit" name="del" value="Yes" />
    <input type="submit" name="del" value="No" />
</div>
</form>
<?php else: ?>
<p>Cannot find album.</p>
<?php endif;?></b>
<?php echo $this->render('footer.phtml'); ?>

Устранение неполадок


Если возникают сложности при обращении к любым действиям помимо index/index, скорее всего причина состоит в том, что класс-роутер не может корректно определить, в какой директории находится ваш веб-сайт. Такая ситуация может возникнуть, если URL сайта не соответствует пути к его директории относительно корневого каталога, открытого для доступа из сети.
Если приведенный выше код не соответствует вашему случаю, необходимо задать переменной $baseURL корректное для вашего сервера значение:

zf-tutorial/index.php:

...
// setup controller
$frontController = Zend_Controller_Front::getInstance();
$frontController->throwExceptions(true);
$frontController->setBaseUrl('/mysubdir/zf-tutorial');
$frontController->setControllerDirectory('./application/controllers');
...

Значение /mysubdir/zf-tutorial/ понадобится заменить на действительный путь к файлу index.php. Например, если ваш URL к index.php выглядит как localhost/~ralle/zf-tutorial/index.php, корректным значением для переменной $baseUrl будет /~ralle/zf-tutorial/.

Заключение


На этом построение простого но полнофункционального MVC-приложения можно считать завершенным. Надеюсь, этот обзор был полезен и информативен для вас. Любые замечания к оригинальному тексту можно отправлять автору статьи по адресу rob@akrabat.com. Комментарии, связанные с русским переводом, отправляйте на musayev@yandex.ru.

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

При переводе использовались материалы свободной энциклопедии Wikipedia.org.
Tags:
Hubs:
+19
Comments29

Articles