Аллоха, ювижинцы! Ниже наглая копипаста с моего стандалона. Понеслась.
Сегодня мы продолжим изучать Zend Framework. В прошлом посте мы с вами кратко познакомились с ним, установили и вывели стандартное приветствие в браузер. Пришло время разработать свое первое приложение.
Я решил пропустить традиционное "Hello, World!", выбрав не менее традиционный блог :). На его примере, думаю можно хорошенько разобрать многие более-менее используемые компоненты фреймворка.
Итак, составим нечто вроде ТЗ, чтобы ясно представлять себе будущий функционал.
- Список постов на главной с постраничной навигацией. При клике на заголовок, должна открываться полная версия. Каждый пост имеет свою категорию. Категории выводятся где-нибудь в сайдбаре и при клике должен выводится список постов этой категории, также с постаничной навигацией.
- К каждому посту можно оставить комментарий.
- Аутентификация администратора.
- CRUD категорий, постов и комментов. Админки как таковой не будет, просто админу будут выводится ссылки на CRUD.
В принципе все. Эдакий simple blog. Если у вас имеются какие-нибудь предложения по дополнительной функциональности - велкам в комменты. Теперь приступим к самому вкусному - кодингу. Далее предпологается, что вы уже установили и создали свой проект при момощи Zend_Tool. Теперь взгянем на сгенерированную структуру проекта.
С непривычки такое обилие папок может запутать и напугать. Но немного привыкнув, все станет понятно и логично. В configs хранится конфигурация вашего проекта, в controllers, models, views, как можно догадаться 3 составляющие части MVC - контроллеры, модели и вьюшки. В docs документация, в library дистрибутив ZF (если вы последовали совету из предыдущего поста, то эта папка у вас должна быть пуста, т.е. она и так будет пуста, пусть такой и остается), в public вся доступная извне часть вашего приложения (CSS-стили, JS-скрипты, картинки, etc), в tests всякие тесты. Скрытый файл .zfproject.xml в корне - описание структуры вашего приложения.
Ну-с, со структурой более-менее разобрались, двигаемся дальше. Попытаюсь на пальцах объяснить принцип работы ZF. В каталоге public лежат два файла - .htaccess и index.php. Первый перенаправляет все запросы на единственный входной файл index.php, который реализует паттерн Front Controller. В index.php обьявляются всякие нужные константы, типа корня приложения. Далее создается экземпляр класса Zend_Application, у которого вызываются поочередно методы bootstrap() и run(). Вдаваться, что делают эти методы мы пока не будем, потому что я и сам толком не знаю, но если коротко, то инициализируют начальные настройки и запускают роутер (маршрутизатор). В корне каталога приложения application лежит один файл Bootstrap.php. Это класс, наследующийся от Zend_Application_Bootstrap_Bootstrap. Он используется для действий при запуске приложения. Далее в configs лежит файл конфигурации application.ini. Данные в нем хранятся в формете .ini и используют наследование секций. В первой секции production хранятся настройки production-приложения, т.е. уже запущенного реального проекта. Остальные секции наследуются от production и служат для тестирования и разработки соответственно. Давайте занесем наши данные для подключения к БД. В конец секции production добавляем:
; DB config resources.db.adapter = PDO_MYSQL resources.db.params.host = localhost resources.db.params.username = root resources.db.params.password = resources.db.params.dbname = zftest resources.db.params.charset = utf8
Думаю здесь все понятно. Обычные данные для подключения к БД. Последней стоит кодировка. Мы будем юзать utf8. Также если у вас Денвер, поменяйте в httpd.cong windows-1251 на utf8. В редакторе кода, кстати тоже установите эту кодировку. Стоит отметить, что мы будем использовать MySQL через PDO, так что у вас должен быть установлен соотвествующий модуль. У меня по умолчанию установлено PDO_MySQL, на Денвере по моему PDO_SQLite. Так что посмотрите вывод фунции phpinfo() и доустановите отсутствующие модули. Да, и еще: комментарии в .ini файлах ставятся через точку с запятой (;), что удобно в отличие например от XML, где комментариев нет. Теперь давайте взглянем на структура БД нашего блога:
CREATE TABLE IF NOT EXISTS `zf_posts` ( `id` MEDIUMINT(5) UNSIGNED NOT NULL AUTO_INCREMENT, `category_id` TINYINT(2) UNSIGNED NOT NULL, `url` VARCHAR(100) NOT NULL, `title` VARCHAR(100) NOT NULL, `blog_post` MEDIUMTEXT NOT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE = MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE TABLE IF NOT EXISTS `zf_categories` ( `id` TINYINT(2) UNSIGNED NOT NULL AUTO_INCREMENT, `url` VARCHAR(100) NOT NULL, `title` VARCHAR(100) NOT NULL, PRIMARY KEY (`id`) ) ENGINE = MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE TABLE IF NOT EXISTS `zf_comments` ( `id` MEDIUMINT(7) UNSIGNED NOT NULL AUTO_INCREMENT, `post_id` MEDIUMINT(5) UNSIGNED NOT NULL, `name` VARCHAR(50) NOT NULL, `email` VARCHAR(50) NOT NULL, `url` VARCHAR(50), `blog_comment` TEXT NOT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE = MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
Этот файл с полным дампом БД под названием schema.sql, вы найдете в прилагаемом архиве, в папке docs. Здесь у нас имеются 3 таблицы: посты, категории и комменты. На них останавливаться не будем.
Начнем с моделей. Здесь будут храниться наши CRUD-методы. Переходим в директорию models и создаем в ней новый каталог DbTable. В нем будут храниться наши модели. Стоит рассказать принцип именования моделей. Каждая модель соответствует одной таблице в БД. Называть класс нужно по назвнию таблицы с префиксом Model. Например наша модель постов будет называться Application_Model_DbTable_Posts. Здесь Application это каталог нашего приложения, Model папка с моделями, DbTable папка где храняться модели наших таблиц и наконец Posts - название таблицы. Довольно длинное название, но если вы ознакомились со стандартами кодинга Zend Framework, а я настоятельно рекомендую сделать это, то вы поймете, что к чему. Сделано это для имитации пространства имен. В PHP 5.3 наконец появилась нативная поддержка неймспейсов, думаю в будущих версиях ZF начнут их применять. Нам нужно получать пост по ID, для вывода отдельного поста. Вот листинг модели постов:
<?php /** * Posts model * * @author Kanat Gailimov, http://gailimov.info * @copyright Copyright (c) Kanat Gailimov (http://gailimov.info) 2011 */ class Application_Model_DbTable_Posts extends Zend_Db_Table_Abstract { /** * Db table name * * @var string */ protected $_name = 'zf_posts'; /** * Get post by ID * * @param int $id ID of post * @return array */ public function getById($id) { $row = $this->fetchRow('id = ' . $id); if (!$row) { throw new Exception('Ахтунг! Выборка поста не удалась :('); } return $row; } }
Код подробно прокомментирован, но думаю пояснить все же стоит. Первым делом создаем класс, наследующийся от Zend_Db_Table_Abstract. Делее создаем protected свойство, в котором будет храниться название нашей таблицы. Затем создаем метод для выборки постов по ID'шнику, в который передаем этот самый ID'шник. Для этого используе метод fetchRow() класса Zend_Db_Table_Abstract. Этот метод возращает одну строку в таблице.
Займемся контроллерами. По умолчанию ZF генерирует для нас два файла с контроллерами: ErrorController.php и IndexController.php. Первый для обработки ошибок, второй собственно для отображения страниц. URL'ы в ZF строятся таким образом: http://sitename.loc/controller/action/params, где controller - контроллер, action - метод или как его еще называют экшен (действие), params - параметры. Контроллеры именуются по схеме - ControllerNameController, т.е. в camel case (при том первая буква заглавная) и плюс в конце обязательно добавляется Controller. Сам класс называется точно так же как файл, т.е. здесь не действуют стандартные имитаторы пространств имен ZF, и наследуется от Zend_Controller_Action. Методы именуются по схеме methodAction, т.е. название метода в нижнем регистре плюс слово Action. Мы будем использовать сгенерированный indexController. В нем уже содержатся два метода init() и indexAction(). Нам также нужен еще один метод для просмотра отдельного поста. Можно объявить его в коде ручками, но мы воспользуемся Zend_Tool. Вы спросите почему? Ну чтобы показать как можно генерировать методы, и еще Zend_Tool помимо самих экшенов создаст нам соответствующие вьюшки. Итак, открываем консоль, переходим в директорию с проектом и вводим команду:
zf create action post index
Здесь post - название экшена (будет postAction()), index - контоллер. В каталоге application/views/scrips/index/ появится файл post.phtml. Это вид для нашего экшена. В ZF вьюшки это php-файл, с окончанием .phtml. Но о видах потом. Сейчас давайте посмотрим на код нашего контроллера:
<?php class IndexController extends Zend_Controller_Action { public function init() { // Уставливаем название блога $this->view->title = 'Тестовый блог на Zend Framework'; // Устанавливаем разделитель для тега title с помощью хелперов // headTitle() и setSeparator() $this->view->headTitle()->setSeparator(' | '); // Передаем заголовок в тег title, с помошью хелпера headTitle() $this->view->headTitle($this->view->title); } public function indexAction() { // Создаем экземпляр модели постов $posts = new Application_Model_DbTable_Posts(); // Выбираем все посты // Формируем условие $select = $posts->select()->order('created_at DESC') ->order('id DESC'); // Выполняем запрос $this->view->posts = $posts->fetchAll($select); } /** * View post * * @return void */ public function postAction() { // Берем ID'шник из параметра if ($id > 0) { // Создаем экземпляр модели постов и выбираем посты по ID $post = new Application_Model_DbTable_Posts(); $this->view->post = $post->getById($id); // Устанавливаем заголовок для поста в тег title $this->view->postTitle = $this->view->post['title']; $this->view->headTitle($this->view->postTitle); } } }
Поясню код. Сначала обьявляем наш класс IndexController, наследующийся от Zend_Controller_Action. Далее объявляем открытые методы. init() - это действия, которые будут выполняться всегда. Типа констуктора. Сюда мы устанавливаем заголовок блога и разделитель. indexAction() - действие для главной страницы. Здесь мы выбираем посты с помощью метода fetchAll() класса Zend_Db_Table_Abstract. postAction() - показ отдельного поста. Здесь мы получаем параметр с помощью метода _getParam() и, если ID больше нуля выбираем пост, с помощью нашего ранее созданного метода getPostById($id).
Настало время третьей составляющей паттерна MVC, а именно view. За вид в Zend Framework отвечает компонент Zend_View. Мы для удобства будем использовать layout'ы, т.е. макеты в нашем виде. Фишка layout'ов, в том, что не нужно в каждом файле представления писать что-то типа include 'inc/header.php'. Вместо этого создается отдельный файл в котором описывается главный шаблон. В нужных местах прописываются переменные, в которые будут помещаться динамические части. Чтобы создать лайаут воспользуемся Zend_Tool. Прописываем в консоли команду:
zf enable layout
Эта команда создаст каталог application/layout/scripts и поместит в него файл layout.phtml. Также в application.ini будет добавлена строчка:
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts/"
Приведу код нашего макета:
<!DOCTYPE html> <html> <head> <meta charset=utf-8> <!--[if IE]> <script> document.createElement('header'); document.createElement('nav'); document.createElement('section'); document.createElement('article'); document.createElement('aside'); document.createElement('footer'); </script> <![endif]--> <link rel="stylesheet" href="<?php echo $this->baseUrl() ?>/css/style.css" media="screen" /> <?php echo $this->headTitle() ?> </head> <body> <div id="wrapper"> <header id="header"> </header> <?php echo $this->layout()->content ?> <aside> <section> <header> </header> <ul> </ul> </section> </<footer> </footer> </div> <!-- wrapper --> </body> </html>
Здесь объявляется стандартный доктайп HTML 5. Конечно, можно воспользоваться хелпером ZF, но я предпочел написать ручками. Далее подключаются css-файл, код которого я приводить не буду, но он будет включен в архив. Заметьте, здесь используется хелпер baseUrl(), который "знает" путь к каталогу нашего приложения. Так что, где бы ни был расположен наш блог, все пути будут правильными. headTitle() - возравщает заголовок нашего блога в теге title. Помните, в контроллере мы устанавливали значение для него? Самое интересное происходит в строке:
<?php echo $this->layout()->content ?>
Сюда будет подставляться наш основной контент. Давайте теперь взглянем на файл со списком постов для главной (views/scripts/index/index.phtml):
<section id="content"> <section id="posts"> <article> <p>Посты закончились</p> </article> <?php else : foreach ($this->posts as $post) : ?> <article> <header> <h2><a href="<?php echo $this->url(array('controller' => 'index', 'action' => 'post', 'id' => $post->id)) ?>"><?php echo $this->escape($post->title) ?></a></h2> </header> <?php echo $post->blog_post ?> <p class="date"><?php echo $this->escape($post->created_at) ?></p> </article> <?php endforeach; endif ?> </section> <!-- posts --> </section> <!-- content -->
Здесь ничего сложного, обычная проверка на массива с постами и вывод в цикле foreach. Новымы можгут быть лишь хелперы url() и escape(), служащие для построения ссылок и escape() для экранирования HTML-тегов. В параметры первому передается ассоциативный массив со значениями контроллера, экшена и параметры экшена.
А вот вьюха для отдельного поста (views/scripts/index/post.phtml):
<section id="content"> <section id="posts"> <article> <header> <h2><?php echo $this->post['title'] ?></h2> </header> <?php echo $this->post['blog_post'] ?> <p class="date"><?php echo $this->post['created_at'] ?></p> </article> </section> <!-- posts --> </section> <!-- content -->
Теперь наконец можете набрать в браузере адрес своего проекта и посмотреть результат. В следующем посте мы сделаем вывод категорий и постов в них. Может и комментарии захватим. Так что подписывайтесь на RSS. Архив с исходниками можно забрать отсюда.
P.S. Критика, разрыв поста в клочья приветствуется :).