Неоднократно я сталкивался с мнением, что в мануале по Zend Framework
очень мало туториалов, то есть в нем хорошо описаны отдельные классы,
но тяжело разобраться как их использовать вместе. Сейчас я попытаюсь
восполнить этот недостаток. Статья предназначена для начинающего и
среднего уровня. После прочтения вы сможете создавать простые сайты на
базе Zend Framework. В конце вы сможете скачать
полноценный рабочий код мини сайта - примера, разработанного специально
под статью. Код был протестирован на Zend Framework 1.5.2, php 5.2.0,
mysql 5.0, apache 1.3
В сайте реализовано:
Паттерн MVC - на основе Zend_Controller, Zend_View, Zend_Layout и Zend_Db
Управление конфигурационными файлами - Zend_Config
Работа с базой данных - Zend_Db, Zend_Db_Select, Zend_Db_Table
Отделение представления (вида) с помощью системы шаблонов - Zend_View
Двухэтапное представление - Zend_Layout
Управление маршрутами - Zend_Controller_Router_Rewrite
Автоматическая загрузка классов - Zend_Loader
Работа с исключениями и обработчик ошибок
Старт
Для получения базовых навыков прочтите предыдущую статью.
Там мы располагали php файлы вне document root директории, но учитывая,
что наши хостеры часто не дают такой возможности, тепер мы будем
располагать все файлы внутри document root.
Итак, скачиваем последнюю стабильную версию фреймворка, разархивируем и
выбираем классы для работы. Почти все классы Zend Framework можно
использовать независимо, поэтому выберем только нужные. Вот так
выглядит наш список файлов и папок библиотеки Zend:
Рис. 1. Файловая структура Zend Framework
А вот так выглядит дерево папок сайта:
Рис. 2. Дерево папок сайта
Во многом структура осталась такая же как в прошлой статье. Все
названия папок полностью соответствуют файлам, что лежат в них,
поэтому, комментировать их не буду. Отмечу только что в папку system я
кладу системные файлы, которые трудно отнести к какой-либо части MVC.
Для лучшего понимания вы можете сразу скачать код, смотреть и сравнивать. Кроме того код обильно откомментирован.
Общий алгоритм работы сайта
Для общего понимания я схематически изобразил алгоритм работы сайта.
Рис. 3. Алгоритм работы сайта
Как и ранее с помощью файла .htaccess все запросы к сайту перенаправляются на входную точку - файл index.php лежащий в корне document root.
Не перенаправляются только запросы на реально существующие файлы,
поэтому во избежание неприятных ситуаций сразу закрываем доступ в папку
applications где расположены php файлы. Для этого кладем туда .htaccess
запрещающий доступ к любым файлам.
// Пути по которым происходит поиск подключаемых файлов, это папка библиотек, моделей и системных файлов set_include_path($paths);
// Подключение главного системного класса require'Kernel.php';
// Запуск приложения Kernel::run($config);
Так как мы не хотим каждый раз указывать полный путь к нужным файлам - задаем set_include_path. Подключаемые файлы это библиотеки Zend, системные классы, а также классы модели. index.php запускает Kernel::run() - основной метод нашего "ядреного" класса Kernel.php. Этот класс предназначен для инициализации и настройки окружения.
// Создание объекта конфигурации $cnf = new Zend_Config($config);
// Занесение объекта конфигурации в реестр Zend_Registry::set('cnf', $cnf);
// Подключение к базе данных self::setDbAdapter();
// Подключение файла с правилами маршрутизации if (file_exists($cnf->path->system.'routes.php')) { require($cnf->path->system.'routes.php'); }
// Инициализация Zend_Layout, настройка пути к макетам, а также имени главного макета. Zend_Layout::startMvc(array( 'layoutPath' => $cnf->path->layouts, 'layout' => 'index', ));
// Получение объекта Zend_Layout $layout = Zend_Layout::getMvcInstance();
// Инициализация объекта Zend_View $view = $layout->getView();
// Настройка расширения макетов $layout->setViewSuffix('tpl');
// Задание пути для view части $view->setBasePath($cnf->path->views);
// Установка объекта Zend_View $layout->setView($view);
// Настройка расширения view скриптов с помощью Action помошников $viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer(); $viewRenderer->setView($view) ->setViewSuffix('tpl');
Практически каждая строчка откомментирована и уже сейчас вы можете полностью представить себе схему работы сайта-примера.
Далее, предположим мы обратились к следующей странице нашего сайта http://ilovezf/pages/1/ В этом случае будет вызван IndexController и его действие PageAction. (Такое поведение происходит потому что я добавил специальное правило в файл маршрутов routes.php, по умолчанию по такому запросу вызовется контроллер PagesController и действие 1, конечно если они существуют)
В контроллере происходит подключение модели и получение данных
необходимых запрошенной странице. Затем происходит определение
переменных для вида.
Наконец в главный макет applications/views/scripts/index.tpl подставляются нужные нам данные и весь полученный результат отправляется в браузер.
Далее я более подробно остановлюсь на ключевых моментах построения нашего приложения.
Zend_Loader
Этот класс позволяет использовать одну из замечательных возможностей php5 - автоматическую загрузку объектов, иначе называемую autoload.
Если вы пишите ООП приложение то у вас, должно быть, не один десяток
различных классов, и на некоторых страницах нужно подключить 5-10
классов. Если таких страниц много, то это очень неприятно. С
Zend_Loader вам достаточно всего лишь указать название класса в нужном
формате, и он сам произведет подключение.
То есть, в коде например пишем
$news = new Text_News_Home();
Символ "_" используется как разделитель, Zend_Loader будет пытаться найти указанный класс по адресу Text/News/Home.php.
(В рассматриваемом примере в index.php я указал относительно каких директорий будет происходить поиск)
Для автоматической загрузки можно использовать метод Zend_Loader::loadClass() но я рекомендую выполнять Zend_Loader::registerAutoload(); что бы не писать каждый раз Zend_Loader::loadClass(). Правда registerAutoload() требует наличие модуля spl_autoload.
Zend_Registry
Класс реализует паттерн singleton
и таким образом позволяет нам обращаться к нужным объектам из любого
места кода. Это альтернатива использованию глобальных переменных. Класс
оперирует неким реестром, в который заносятся объекты. Ключевые методы
класса это get и set,
которые заносят и получают объекты в реестр соответственно. В
сайте-примере в реестр заносятся объекты соединения с БД и объект
конфигурации. Также важен метод isRegistery проверяющий существование объекта в реестре.
Zend_Config
Класс позволяет в ООП стиле работать с конфигурационными файлами.
Поддерживаются конфигурационные файлы различного формата: php-массив,
ini - файл, xml - файл. Для рассматриваемого примера я выбрал самый
простой вариант - хранить настройки в php массиве. Вы можете
использовать любой. В объекте Zend_Config все данные представлены в
виде ассоциативного массива. Например таким образом:
$cnf->path->system
получаем значения физического пути папки с системными файлами.
Например вот так может выглядеть ваш конфигурационный файл:
Управление маршрутами Zend_Controller_Router_Rewrite
Маршрутизация
- это процесс принятия URI (той части URI, которая идет после базового
URL) и ее разложения на части для определения того, какой контроллер и
какое действие этого контроллера должны получить запрос и выполниться.
С помощью Zend_Controller_Router_Rewrite можно задавать различные
правила маршрутизации.
С помощью указанных правил мы добиваемся например того что по запросу http://ilovezf/articles/1/ будет вызван контроллер articlesController и действие viewAction, кроме того значение идентификатора статьи (в указанном запросе это 1) будет доступно по параметру articleId.
Кроме Zend_Controller_Router_Route который используется для
динамических маршрутов, есть еще Zend_Controller_Router_Route_Static
для постоянных неизменных маршрутов, например для URL типа
http://ilovezf/exit/.
Также существует Zend_Controller_Router_Route_Regex основанный на
регулярных выражениях, это самый гибкий способ определения маршрутов.
Front контроллер
Zend_Controller_Front реализует
паттерн Front Controller, используемый в приложениях MVC. Его
назначение состоит в инициализации окружения запроса, проложении
маршрута приходящего запроса и последующем запуске выявленных действий.
Он агрегирует все ответы и возвращает их по завершении процесса.
// Создание объекта front контроллера $front = Zend_Controller_Front::getInstance();
// Настройка front контроллера, указание базового URL, правил маршрутизации $front->setBaseUrl($cnf->url->base) ->setRouter($router);
// Запуск приложения, в качестве параметра передается путь к папке с контроллерами Zend_Controller_Front::run($cnf->path->controllers);
Работа с исключениями и обработчик ошибок
Zend Framework предлагает несколько вариантов работы с исключениями. Во
первых это плагин Zend_Controller_Plugins_ErrorHandler, он дает
возможность перехватывать исключения вызванные отсутствием контроллера
или действия, кроме того он перехватывает исключения выброшенные в
контроллерах. Никакие другие исключения, например исключения
маршрутизации, или исключения выброшенные в других плагинах он не
перехватывает. Этот плагин хорош именно для работы с ошибками типа 404,
он позволяет в рамках общей структуры макетов отобразить страницу 404 с
необходимыми сообщениями. За плагин отвечает контроллер ErrorController и действие errorAction.
Для того же что бы все таки обрабатывать остальные ошибки в рассматриваемом примере используется еще один обработчик ошибок /applications/system/Error.php.
// Получение текста ошибки $message = $exception->getMessage(); // Получение трейса ошибки как строки $trace = $exception->getTraceAsString(); $str = 'ERROR: ' . $message . "n" . $trace;
$cnf = Zend_Registry::get('cnf');
// Если включен режим отладки отображаем сообщение о ошибке на экран if($cnf->debug->on) { Zend_Debug::dump($str); } // Иначе выводим сообщение об ошибке else { // Здесь может происходить логирование ошибки, уведомление вебмастера и т д die('System error! Please try later'); } } }
В Kernel.php с помощью try и catch
все ошибки перехватываются и передаются этому обработчику. Далее в
зависимости от настроек режима дебага в конфигурационном файле, либо
показывается информация об ошибке в браузере, либо выдается сообщение о
временных неполадках.
Для того что бы все ошибки, включая ошибки не найденных
контроллеров и действий, перехватывать своим обработчиком, необходимо
при настройке front контроллера выполнить $front->throwexceptions(true).
Работа с контроллером
Контроллер является связующим звеном между видом и моделью. Основная
задача контроллера это проверить данные что пришли от пользователя,
выполнить те или иные действия с данными, используя методы модели, и
наконец определить переменные для вида. Для иллюстрации привожу
откоментированный код контроллера ArticlesController
class ArticlesController extends Zend_Controller_Action {
/** * Список статей */ publicfunction indexAction() {
// Создание объекта модели, благодаря autoload нам нет необходимости подключать класс через require $articles = new ArticlesModel(); // Выполнения метода модели по получению списка статтей $articlesList = $articles->getArticles(); // Определение переменных для вида $this->view->articlesList = $articlesList;
}
/** * Выбранная статья */ publicfunction viewAction() {
// Получение параметра пришедшего от пользователя $articleId = $this->_getParam('articleId'); // Создание объекта модели $articles = new ArticlesModel(); // Выполнения метода модели по получению информации о статье $articleInfo = $articles->getArticles($articleId); // Определение переменных для вида $this->view->articleInfo = $articleInfo;
}
}
Работа с моделью
Работа с моделью практически не освещена в мануале, в файловой
структуре предпологается папка models но не дается конкретных
рекомендаций по ее и спользованию. Кроме того можно сказать что понятия
модели в Zend Framework как такового нет. Реализация этой части MVC
возлагается на плечи разработчика. Я рассматриваю модель как набор
классов оперирующих с данными, это может быть получение данных,
изменения данных, вставка данных в базу данных или другое хранилище. Я
не допускаю составления и выполнения sql-запросов в классах
контроллеров. Название каждого класса модели состоит из двух слов,
самого названия сущности данных и слова "Model", по аналогии к
названиям контроллеров. В рассматриваемом сайте-примере существует два
класса модели, это PagesModel и ArticlesModel.
В первом классе собраны функции работающие с сущностью "страница", во
втором с сущностью "статьи". Классы модели являются наследниками
Zend_Db_Table_Abstract. Методы работы с базой и пример класса модели
смотрите ниже.
Работа с базой данных
Для гибкой работы с БД Zend
Framework предлагает несколько классов. Нам понадобятся Zend_Db,
Zend_Db_Select, Zend_Db_Table, Zend_Db_Table_Select.
В Kernel.php задается соединение с БД, задается адаптер используемый по умолчанию, и объект соединения заносится в реестр.
// Подключение к БД, так как Zend_Db "понимает" Zend_Config, // нам достаточно передать специально сформированный объект конфигурации в метод factory $db = Zend_Db::factory($cnf->db); // Задание адаптера по умолчанию для наследников класса Zend_Db_Table_Abstract Zend_Db_Table_Abstract::setDefaultAdapter($db);
Отмечу что если вы используете Zend_Cache вам пригодится метод Zend_Db_Table_Abstract::setDefaultMetadataCache, с его помощью вы можете закешировать метаданные таблиц. Таким образом запрос describe table не будет выполнятся при каждом создании объекта Zend_Db_Table.
Далее вся работа с базой данных у нас будет собрана в классах моделей.
Для примера рассмотрим код модели ArticlesModel
<?php
class ArticlesModel extends Zend_Db_Table_Abstract {
// Имя таблицы protected$_name = 'articles';
/** * Получить все статьи или одну * * @param int $articleId Идентификатор статьи * @return array */ publicfunction getArticles($articleId = NULL) {
// Создаем объект Zend_Db_Select $select = $this->getAdapter()->select() // Таблица из которой делается выборка ->from($this->_name) // Добавление таблицы с помощью join, указывается поле связи ->join('users','users.id = articles.author_id',array('name')) // Порядок сортировки ->order('id DESC') // Количество возвращаемых записей ->limit(2) ;
if (!is_NULL($articleId)) {
// Условие на выборку $select->where("articles.id = ?",$articleId); // Выполнение запроса $stmt = $this->getAdapter()->query($select); // Получение данных $res = $stmt->fetch();
} else {
$stmt = $this->getAdapter()->query($select); // Получение массива данных $res = $stmt->fetchAll();
}
return$res;
}
}
Класс ArticlesModel наследует
абстрактный класс Zend_Db_Table_Abstract. Таким образом, мы получаем
возможность использовать его функционал по работе с базой.
Zend_Db_Table реализует паттерн Table Data Gateway. Идея в том что для
каждой таблицы создается отдельный класс который предназначен для
работы именно с этой таблицей. Таким образом, работа с БД делится на
логические части, с которыми, в случае большого объема проекта,
работать гораздо удобнее.
Например, вся работа со статьями в сайте-примере собрана в модели ArticlesModel, а работа со страницами собрана в модели PagesModel.
Все что нам нужно, это указать название таблицы с которой мы работаем: protected $_name = 'articles';
Для различных сложных выборок из базы используется класс
Zend_Db_Select, он предоставляет гибкий функционал для выполнения
select запросов. Особенно отмечу объектно-ориентированный подход
"покусочного" построения sql-запросов. Например:
"Куски" могут распологаться в произвольном порядке, это очень удобно когда у вас запросы строятся динамически.
Для защиты от sql-инъекций в select запросах предлагается механизм плейсхолдеров where('id = ?',$pageId);
В случае же update, delete запросов использующих условие where
c данными полученными от пользователя, необходимо поступать немного по
другому. Для защиты от sql-инъекций в таких случаях нужно экранировать
спец символы и брать данные в кавычки самому. Я использую следующий
метод: $id = $this->getAdapter()->quote($id);
Если запросы на выборку у вас достаточно простые и не требуют данных из
других таблиц, вы можете использовать методы fetchRow, fetchAll класса
Zend_Db_Table (обратите внимание что fetchAll и fetchRow класса Zend_Db_Table теперь рекомендуются к использованию с Zend_Db_Table_Select), пример:
<?php /** * Работа с страницами */ class PagesModel extends Zend_Db_Table_Abstract {
// Имя таблицы protected$_name = 'pages';
/** * Получить одну страницу (альтернативой этому методу может быть встроенный метод find) * * @param int $pageId Идентификатор страницы * @return array */ publicfunction getPage($pageId) {
// Создание объекта Zend_Db_Table_Select, // Нам не нужно указывать название таблицы как в Zend_Db_Select $select = $this->select() // Накладываем условие ->where('id = ?', $pageId);
// Выполняем запрос и получаем объект Zend_Db_Table_Row в результате // Нам не нужно предварительно выполнять запрос методом query, как в Zend_Db_Select $row = $this->fetchRow($select); // Преобразовываем Zend_Db_Table_Row в массив $res = $row->toArray();
return$res;
}
Итого, если вам необходимы запросы на вставку, удаление, изменение
данных - нужно использовать Zend_Db_Table. Если вам необходимы простые
запросы на выборку данных результатом которых есть данные только одной
таблицы - можно использовать Zend_Db_Table (а можно и Zend_Db_Select).
Если же вам нужны сложные запросы с получением данных из других таблиц
- нужно использовать Zend_Db_Select.
Работа с видом Zend_View и Zend_Layout
Для работы с видом Zend Framework предлагает два класса, это Zend_View и Zend_Layout.
При создании объектов этих классов в Kernel.php
я произвел небольшую настройку. Изменил расширение этих файлов на
привычное tpl, и изменил название главного layout макета. Я дал ему
привычное название "index"
Вот так выглядит скрипт вида, отвечающий за список статей.
Как видно php код здесь отделен от html кода, кроме того
используется pascal-подобный синтаксис php для лучшего визуального
восприятия.
Zend_Layout реализует паттерн "двухэтапное представление" (Two
Step View pattern). Данный паттерн предназначен для максимально гибкой
работы с макетами вида. Создается некий главный макет дизайна сайта.
Обычно он содержит каркас html страницы
// Doctype <html> <head> // Мета теги // Подключение css и т д </head> <body> ... </body> </html>
А вот все что находится между <body></body>
разбивается на блоки, причем у каждого блока есть свой маленький макет.
Это может быть меню, новости, блок авторизации и т д. Таким образом,
если мы хотим изменить html код меню, мы меняем всего лишь один макет.
Вот так выглядит код главного макета index.tpl
Кроме двухэтапного представления Zend_Layout дает помощников. Это doctype, headTitle, headMeta, headLink, headScript. Эти помощники выручают если например на какой то особой странице вы захотите подключить особый css или js файл.
С помощью помощника partial можно подключать другие макеты - блоки, например в вышеприведенном коде так подключается меню.
Кроме того можно вызывать различные действия прямо из макета. Именно
это дает нам возможность разбить весь шаблон сайта на блоки. Таким
образом, я подключаю блок "Лучшие статьи", и когда я захочу убрать его
со всех страниц, я удалю только одну строчку, а если я захочу изменить
его формат, я отредактирую всего один view скрипт.
Возможно в качестве шаблонизатора вы захотите использовать
Smarty или что нибудь в этом роде. Интеграция с Zend Framework не
вызовет большого труда. Но хочу отметить что Zend_View и Zend_Layout
уже сами по себе отличный шаблонизатор, причем не требующий
использования нового специфического языка шаблонизатора.
Заключение
Скачать код без библиотек Zend (16 kb) Скачать код с библиотеками Zend (368 kb)
Для запуска кода необходимо:
Не забыть что Zend Framework требует php 5.1.4 или выше
Установить расширение PDO с поддержкой драйвера mysql (если оно не установлено)
Скачать
архив, разархивировать его в document root виртуального хоста. Сайт
будет работать только в document root, но не во вложенной папке.
Создать базу данных в кодировке UTF-8
Выполнить sql dump расположенный в корне архива
Отредактировать конфигурационный файл applications/settings/config.php Необходимо указать название БД, имя пользователя и пароль
Результат работы разработанного сайта примера должен выглядеть примерно следующим образом.
Рис. 4. Внешний вид сайта-примера
P.S.
Весь вышеизложенный подход построения приложения на базе Zend Framework
есть субъективным мнением автора и не претендует на полную
объективность и оптимальность. Главной целью данной статьи было
показать, как можно использовать Zend Framework для построения web
приложения. Не весь код приведенный в статье присутствует именно в
таком виде в архиве, в некоторых местах в статье он не существенно
изменен, для лучшей наглядности.
P.P.S.
Приношу благодарность Grisha Kostyuk aka naspeh за помощь в написании статьи.
P.P.P.S. Если статья показалась вам полезной, вы можете помочь развитию
русскоязычному сообществу zend framework, поставив ссылку на ресурс http://zendframework.ru.
В следующий раз я планирую расширить пример и описать работу с
Zend_Auth, Zend_Acl, Zend_Session, Zend_Cache, Zend_Date а также
добавить работу с Ajax