Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Хабрапост 2007 года про Zend Framework — http://habrahabr.ru/post/31168/, http://habrahabr.ru/post/31173/
<b>Update (2014):</b> Это статья 2007 года, которая, к моему удивлению, до сих пор пользуется спросом. По этой причине я обновил её в соответствии с новыми правилами оформления постов на Хабре, и добавил подсветку синтаксиса для примеров кода. Если кому-то захочется что-то добавить или исправить, исходник текста с хабра-разметкой выложен в открытый доступ: https://gist.github.com/dreikanter/2b4ee996d7a775e707d7
<h4>Аннотация от переводчика</h4>
PHP — один из самых широко распространенных языков разработки веб-приложений и при этом один из самых спорных. Я очень часто видел негативное отношение к этой технологии, да и недостатки, провоцирующие это отношение — не для кого не являются секретом. Тем не менее, PHP активно эволюционирует и во многих отношениях постепенно становитс лучше. Одним из серьезных шагов его развития, на мой взгляд, является появление MVC-фреймворков, призванных систематизировать процесс разработки веб-приложений и приучить к порядку разработчиков, которым зачастую здорово не хватает силы воли, чтобы при всей предоставляемой языком свободе, сохранить грамотную и красивую инфраструктуру разрабатываемого ПО (сразу уточню, что последнее утверждение субъективно и основано исключительно на виденном мной коде различных программных решений).
В последнее время я активно заинтересовался архитектурой MVC и сделал свою собственную реализацию фреймворка на этой концепции для PHP4. Совсем недавно на глаза попался первый официальный релиз Zend Framework, о котором я давно слышал, но все руки не доходили с ним поиграть. Для PHP существуют и другие подобный библиотеки, но в данном случае привлек бренд. <habracut>
Вначале я испытал некоторое разочарование в официальной документации, которая оказалась хорошим reference-ом, но просто никаким tutorial-ом и поэтому на роль вводного материала не подошла. Но почти сразу нашлась неплохая статья, в которой автор пошагово разбирает процесс создания веб-приложения. Материала достаточно, чтобы разобравшись в нем можно было начать осмысленно ориентироваться в мане от Zend. Текст ориентирован на программистов, имеющих некоторый опыт в программировании веб-приложений на PHP, но не знакомых с Zend Framework.
Статья довольно объемная, поэтому я решил разделить ее на две части, дабы не перегружать никому мозг. Ниже я привожу первую часть ее перевода, в которой в общих чертах освещена концепция MVC и разобрана структура веб-приложения, реализованного на Zend Framework. Если материал заинтересует хабрачитателей, я опубликую продолжение.
В завершение введения, небольшой дисклаймер. Уважаемые любители Ruby on Rails и Python on Django, я уважаю вашу религию :) Убедительная просьба не развивать в комментариях тему о том, какой фреймворк или язык лучше. Это совсем не относящаяся к топику данного поста тема. Мне и, вероятно, многим сейчас будет гораздо интереснее узнать об опыте реальных разработок ПО на Zend.
<b>Автор:</b> Роб Ален, http://akrabat.com
<b>Оригинал:</b> http://akrabat.com/zend-framework-tutorial/
<b>Перевод:</b> Александр Мусаев, http://musayev.com
Материал рассчитан на то, чтобы дать общее представление об использовании Zend Framework для создания простейших приложений с использованием баз данных. Приведенные примеры были протестированы на Zend Framework версии 1.0.0. Скорее всего, они будут работать и с более поздними версиями, но не с более ранними.
<h4>Архитектура Модель-Вид-Контроллер</h4>
PHP-приложение, написанное традиционным способом, может представлять собой некое подобие следующего примера:
<source lang="php"><?php
include "common-libs.php";
include "config.php";
mysql_connect($hostname, $username, $password);
mysql_select_db($database);
?>
<?php include "header.php"; ?>
<h1>Home Page</h1>
<?php
$sql = "SELECT * FROM news";
$result = mysql_query($sql);
?>
<table>
<?php while ($row = mysql_fetch_assoc($result)) { ?>
<tr>
<td><?php echo $row['date_created']; ?></td>
<td><?php echo $row['title']; ?></td>
</tr>
<?php } ?>
</table>
<?php include "footer.php"; ?>
</source>
В ходе жизненного цикла приложений подобного типа, трудоемкость их поддержки становится крайне высока, т. к. меняющиеся требования заказчика приводят к необходимости вносить большое количество изменений («заплаток») в исходный код. Это делает его плохо структурированным и трудно читаемым. Один из методов для повышения гибкости приложения состоит в разделении кода на три категории:
<ul>
<li><b>Модель.</b> Моделью называют ту часть приложения, которая относится к работе с данными. в приведенном выше фрагменте кода, это реализация новостной ленты. Подобные модели широко применимы в управляющей логике приложений, имеющих в своей основе базы данных.</li>
<li><b>Вид.</b> Под термином «вид» подразумевается пользовательский интерфейс приложения.</li>
<li><b>Контроллер.</b> Контроллеры реализуют задачи, связанные со взаимодействием между моделью и видом.</li>
</ul>
Zend Framework основан на программной архитектуре Модель-Вид-Контроллер (Model-View-Controller). Ее суть состоит в разделении приложения на перечисленные выше три отдельные компоненты таким образом, что модификация каждого из них оказывает минимальное воздействие на остальные. Это приводит к существенному облегчению процесса разработки и поддержки.
<h4>Системные требования</h4>
У Zend Framework следующие требования:
<ul>
<li>PHP версии 5.1.4 или выше;</li>
<li>Веб-сервер, поддерживающий mod_rewrite.</li>
</ul>
В данной статье используется сервер Apache.
<h4>Где скачать Zend Framework</h4>
Zend Framework доступен для свободного скачивания в виде ZIP или TAR.GZ архивов по адресу http://framework.zend.com/download/stable.
<h4>Структура директорий</h4>
На данный момент в Zend Framework жестко не стандартизирована структура директорий приложения, но в официальной документации рекомендуется использовать общепринятую схему. Эта схема основана на том, что пользователь, как предполагается, имеет полный доступ к конфигурированию сервера Apache. Мы же будем использовать немного видоизмененный подход, чтобы смягчить данные требования и сделать Zend Framework более применимым в условиях широко распространенного разделяемого хостинга.
Для начала, создайте директорию <code>zf-tutorial</code> в корневом каталоге вашего тестового веб-сайта. Это будет означать, что URL этой директории примет вид <code>http://localhost/zf-tutorial</code> (адрес может варьироваться, в зависимости от настроек вашего сервера).
После этого дополнительно создайте следующую структуру каталогов для хранения файлов веб-приложения:
<source>zf-tutorial/
/application
/controllers
/models
/views
/filters
/helpers
/scripts
/library
/public
/images
/scripts
/styles</source>
Как можно понять из названий, мы выделили специальные директории для файлов моделей, видов и контроллеров приложения. Графика, скрипты и CSS файлы будут храниться в отдельных подкаталогах, расположенных внутри открытой для публичного доступа директории <code>public</code>.
Для начала, разархивируйте скачанный файл <code>ZendFramework-1.0.0.zip</code> (или <code>.tar.gz</code>) во временную директорию. Все файлы внутри архива находятся в директории <code>ZendFramework-1.0.0</code>. Скопируйте содержимое <code>library/Zend</code> в <code>zf-tutorial/library</code>. Теперь ваша директория <code>zf-tutorial/library</code> должна содержать подкаталог <code>Zend</code>.
<h4>Начальная загрузка</h4>
Контроллер <code>Zend_Controller</code> из библиотеки Zend Framework спроектирован для поддержки сайтов с хорошо читаемыми («чистыми») URL. Для достижения этой цели, все запросы к серверу перенаправляются для обработки на специальный файл <code>index.php</code>, именуемый так же файлом начальной загрузки (bootstrapper). Такой подход обеспечивает централизованную организацию инфраструктуры приложения и гарантирует корректную для его функционирования настройку программного окружения. Это достигается благодаря использованию конфигурационного файла <code>.htaccess</code>, который находится в директории <code>zf-tutorial</code>.
<code>zf-tutorial/.htaccess:</code>
<source>RewriteEngine on
RewriteRule .* index.php
php_flag magic_quotes_gpc off
php_flag register_globals off</source>
Директивой <code>RewriteRule</code> задано очень простое правило перенаправления URL, которое может быть переведено на человеческий язык, как «использовать <code>index.php</code> вместо любого URL». Для обеспечения безопасности так же откорректированы некоторые настройки интерпретатора PHP. в принципе, они уже должны быть заданы соответствующим образом в конфигурационном файле <code>php.ini</code> любого грамотно настроенного сервера, но в нашем примере они дублируются локально для большей надежности. Обратите внимание на то, что директива <code>php_flag</code> внутри файла <code>.htaccess</code> может применяться только при использовании <code>mod_php</code>. Если вы используете PHP через CGI или FastCGI, необходимо удостовериться в правильной настройке <code>php.ini</code>.
Вопреки сказанному выше, запросы, относящиеся к графическим файлам, JavaScript и CSS, не должны быть перенаправлены на <code>index.php</code>. Учитывая, что все они хранятся в отдельной директории <code>public</code>, мы с легкостью можем настроить Apache, чтобы сервер отдавал их напрямую. Для этого необходимо создать еще один файл <code>.htaccess</code> в директории <code>public</code> и задать в нем соответствующую директиву.
<code>zf-tutorial/public/.htaccess:</code>
<source>RewriteEngine off</source>
Для повышения безопасности, можно создать дополнительные файлы <code>.htaccess</code> в директориях <code>zf-tutorial/application</code> и <code>zf-tutorial/library</code>, с одинаковым содержимым.
<code>zf-tutorial/application /.htaccess, zf-tutorial/library/.htaccess:</code>
<source>Deny from all</source>
Эта директива закроет доступ из web к содержимому указанных директорий. (Прим. переводчика: последние два файла можно не создавать вообще, если все, что находится вне директории <code>public</code>, заведомо закрыто глобальными настройками сервера).
Обратите внимание на то, что для использования файлов <code>.htaccess</code>, в настройках сервера Apache (<code>httpd.conf</code>) должна быть задана директива <code>AllowOverride</code> со значением <code>All</code>. Приведенная в данной статье идея использования файлов <code>.htaccess</code> принадлежит Джейсону Майнарду (Jayson Minard) и опубликована в статье «Схема построения PHP-приложений: начальная загрузка (Часть 2)» (http://devzone.zend.com/node/view/id/119). Начинающим разработчика стоит ознакомиться с обеими ее частями.
<h4>Файл начальной загрузки index.php</h4>
Файл начальной загрузки <code>zf-tutorial/index.php</code> содержит следующий код:
<source lang="php"><?php
error_reporting(E_ALL|E_STRICT);
date_default_timezone_set('Europe/London');
set_include_path('.'.PATH_SEPARATOR . './library'
.PATH_SEPARATOR.'./application/models/'
.PATH_SEPARATOR.get_include_path());
include "Zend/Loader.php";
Zend_Loader::loadClass('Zend_Controller_Front');
// setup controller
$frontController = Zend_Controller_Front::getInstance();
$frontController->throwExceptions(true);
$frontController->setControllerDirectory('./application/controllers');
// run!
$frontController->dispatch();</source>
Заметьте, что в конце файла отсутствует закрытие блока PHP кода <code>?></code>, т. к. в нем нет необходимости. Кроме того, наличие закрывающего тага может привести к появлению сложных для выявления ошибок при редиректе функцией <code>header()</code>, если в конце файла по недосмотру останутся лишние пробелы.
Рассмотрим код подробнее.
<source lang="php">error_reporting(E_ALL|E_STRICT);
date_default_timezone_set('Europe/London');</source>
Данные строки обеспечивают вывод интерпретатором всех сообщений о происходящих ошибках (подразумевается, что в конфигурационном файле <code>php.ini</code> параметру <code>display_errors</code> задано значение <code>on</code>). Вторая строка кода задает часовой пояс, согласно стандартному требованию PHP 5.1+. Приведенное в примере значение параметра следует заменить на соответствующее вашему географическому положению.
<source lang="php">set_include_path('.'.PATH_SEPARATOR.'./library'
.PATH_SEPARATOR.'./application/models/'
.PATH_SEPARATOR.get_include_path());
include "Zend/Loader.php";</source>
Zend Framework рассчитан на то, чтобы его файлы находились в <code>include_path</code>, поэтому мы добавляем к значению этого параметра путь к нужной директории. Помимо него, в <code>include_path</code> добавляется директория моделей, чтобы облегчить подключение файлов и из нее.
Файл <code>Zend/Loader.php</code> содержит класс <code>Zend_Loader</code> со статическими функциями, позволяющими подключать любой класс из Zend Framework.
<source lang="php">Zend_Loader::loadClass('');</source>
Метод <code>Zend_Loader::loadClass()</code> подключает класс с заданным именем. Для этого выполняется преобразование указанного имени в путь к соответствующему файлу: символы «_» заменяются на «/», а в конце получившейся строки добавляется расширение <code>.php</code>. Таким образом, <code>Zend_Controller_Front</code> преобразуется в <code>Zend/Controller/Front.php</code>. Если следовать аналогичной схеме построения имен для собственных библиотечных классов, то метод <code>Zend_Loader::loadClass()</code> можно будет использовать и с ними.
Класс, который понадобится нам прежде всего, — первичный контроллер. При его работе, в свою очередь, используется специальный класс-роутер, чья задача состоит в нахождении по заданному URL функции отображения соответствующей веб страницы. Для этого роутере должен быть определен базовый URL файла <code>index.php</code>, относительно которого он сможет выделять URI отдельных страниц.
Базовый URL может автоматически определяется с помощью объекта <code>Request</code>, а для того, чтобы применить его в нашем примере следует воспользоваться методом <code>$frontController->setBaseUrl()</code>.
Далее нам понадобится сообщить первичному контроллеру о местоположении директории с другими контроллерами:
<source lang="php">$frontController = Zend_Controller_Front::getInstance();
$frontController->setControllerDirectory('./application/controllers');
$frontController->throwExceptions(true);</source>
Учитывая, что наша программа предназначена для образовательных целей и функционирует на тестовой системе, можно разрешить все исключения, которые могут происходить при ее работе. По-умолчанию, первичный контроллер будет перехватывать их все до одного и хранить в свойстве <code>_exceptions</code> своего объекта (данному объекту соответствует англоязычный термин Response object). Этот объект используется для хранения всей информации, возвращаемой при обращении к заданному URL, что включает HTTP-заголовок, содержимое веб-страницы и массив произошедших исключений.
Первичный контроллер автоматически выдает заголовок и контент страницы непосредственно перед окончанием своей работы. Тех, кто ранее не встречался с архитектурой веб-приложений, подобной Zend Framework, может привести в некоторое недоумение тот факт, что для отображения сообщений об исключительных ситуациях, их (исключения) необходимо повторно возбуждать. Эта особенность становится актуальной для серверов, работающих в своем штатном режиме, когда вывод сообщений об ошибках на страницах неуместен.
В итоге, после всех выполненных приготовлений, наше приложение может быть запущено:
<source lang="php">// run!
$frontController->dispatch();</source>
Если вы сейчас попытаетесь открыть страницу <code>http://localhost/zf-tutorial/</code>, то увидите сообщение о фатальной ошибке, гласящее примерно следующее:
<code>Fatal error: Uncaught exception 'Zend_Controller_Dispatcher_Exception' with message 'Invalid controller specified (index)' in...</code>
Это говорит о том, что мы еще не до конца сформировали наше веб-приложение. и прежде чем приступить к делу , стоит уяснить, в чем конкретно состоит задача.
<h4>Веб-сайт</h4>
Предположим, что мы хотим создать простую базу данных для хранения информации о компакт-дисках. На главной странице будет отображен список дисков в нашей коллекции, а так же будет предоставлена возможность добавлять новые CD, редактировать и удалять ненужные записи. Для хранения данных, используем базу со схемой, приведенной ниже:
<table width="100%" border="1" cellpadding="10"><tr><th>Fieldname</th><th>Type</th><th>Null?</th><th>Notes</th></tr><tr><td>id</td><td>Integer</td><td>No</td><td>Primary key, Autoincrement</td></tr><tr><td>artist</td><td>Varchar(100)</td><td>No</td><td> </td></tr><tr><td>title</td><td>Varchar(100)</td><td>No</td><td> </td></tr></table>
<h5>Необходимые страницы</h5>
Для реализации перечисленных выше функций, нам понадобятся следующие страницы:
<ul>
<li><b>Начальная («домашняя») страница.</b> Список альбомов со ссылками для их редактирования и удаления. На странице так же будет присутствовать ссылка для добавления новых дисков.</li>
<li><b>Добавить альбом.</b> Страница с формой добавления нового диска в базу.</li>
<li><b>Редактировать альбом.</b> Страница с формой для редактирования записи.</li>
<li><b>Удалить альбом.</b> Страница для запроса пользовательского подтверждения перед удалением диска.</li>
</ul>
<h4>Организация страниц</h4>
Прежде чем приступить к созданию файлов, следует понять, как принято организовывать их в Zend Framework. Согласно принятой терминологии, каждая страница веб-приложения определяется термином «действие».
<b>Примечание переводчика.</b> Небольшое пояснение терминологии. Англоязычным оригиналом используемого в данной статье термина «действие» является слово action. Приведенный перевод на данный момент еще не является устоявшимся понятием, и в некоторых текстах о модели MVC вместо него попадаются иные определения. Например, так же известна прямая (и режущая слух) транслитерация — «экшен».
Действия, в свою очередь, объединяются в специальные группы — «контроллеры». Например, для URL <code>http://localhost/zf-tutorial/news/view</code> контроллером будет <code>news</code>, а действием — <code>view</code>. Контроллеры предназначены для объединения родственных действий. Для упомянутого news, могут быть определены действия <code>current</code>, <code>archived</code> и <code>view</code> (соответственно для отображения последней новости, архива записей и новостной ленты).
В Zend Framework так же используются модули для группировки контроллеров, но наше приложение не настолько велико, чтобы имело смысл их применять.
В контроллерах Zend Framework существует понятие действия по-умолчанию, для которого зарезервировано имя <code>index</code>. Такие действия исполняются при обращении к URL вида <code>http://localhost/zf-tutorial/news/</code> (в данном примере выполняется действие <code>index</code> контроллера <code>news</code>). Помимо исполняемых по-умолчанию действий, так же предусмотрены выбираемые по-умолчанию контроллеры с аналогичным именем — <code>index</code>. Это означает, что URL <code>http://localhost/zf-tutorial/</code> будет соответствовать действию <code>index</code> контроллера <code>index</code>.
Учитывая, что данная статья ориентирована на первое знакомство с Zend Framework, мы не будем пока касаться таких нюансов работы веб-приложения, как авторизация пользователей. Ограничимся реализацией основной функциональности, а именно — набором перечисленных выше страниц. Учитывая, что все они относятся к работе с альбомами, можно отнести их к одному контроллеру. Мы используем стандартный контроллер, а его действия будут выглядеть следующим образом:
<table width="100%" border="1"><tr><th>Страница</th><th>Контроллер</th><th>Действие</th></tr><tr><td>Начальная страница</td><td>Index</td><td>Index</td></tr><tr><td>Добавить альбом</td><td>Index</td><td>Add</td></tr><tr><td>Редактировать альбом</td><td>Index</td><td>Edit</td></tr><tr><td>Удалить альбом</td><td>Index</td><td>Delete</td></tr></table>
<h4>Настройка контроллера</h4>
Приступим к созданию нашего контроллера. в Zend Framework контроллер представляет собой класс с именем типа <code>{Имя_контроллера}Controller</code>. Класс должен быть описан внутри файла с именем <code>{Имя_контроллера}Controller.php</code>, в общей директории контроллеров. Стоит обратить внимание на важный нюанс: первая буква в имени класса-контроллера и имени его файла должна быть заглавной, а все остальные — строчными. Каждое действие должно быть открытой функцией класса-контроллера с именем типа <code>{имя_действия}Action</code>. в данном случае, имена действий должны начинаться со строчной буквы.
Таким образом, наш класс-контроллер будет носить имя <code>IndexController</code> и находиться в файле <code>zf-tutorial/application/controllers/IndexController.php</code>.
<code>zf-tutorial/application/controllers/IndexController.php:</code>
<source lang="php"><?php
class IndexController extends Zend_Controller_Action
{
function indexAction()
{
echo "<p>in IndexController::indexAction()</p>";
}
function addAction()
{
echo "<p>in IndexController::addAction()</p>";
}
function editAction()
{
echo "<p>in IndexController::editAction()</p>";
}
function deleteAction()
{
echo "<p>in IndexController::deleteAction()</p>";
}
}</source>
В данном макете контроллера каждое действие будет выводить свое имя. Это можно протестировать, задавая приведенные ниже URL:
<table width="100%" border="1"><tr><th>URL</th><th>Displayed text</th></tr><tr><td><code>http://localhost/zf-tutorial/</code></td><td>in <code>IndexController::indexAction()</code></td></tr><tr><td><code>http://localhost/zf-tutorial/index/add</code></td><td>in <code>IndexController::addAction()</code></td></tr><tr><td><code>http://localhost/zf-tutorial/index/edit</code></td><td>in <code>IndexController::editAction()</code></td></tr><tr><td><code>http://localhost/zf-tutorial/index/delete</code></td><td>in <code>IndexController::deleteAction()</code></td></tr></table>
Теперь у нас имеется работающий класс-роутер с набором действий, соответствующих каждой странице веб-приложения. При возникновении каких-либо проблем в процессе его создания, обратитесь к разделу «Устранение неполадок» в конце статьи.
А теперь пришло время добавить в наше приложение новый вид.
<h4>Вид</h4>
Класс Zend Framework, на базе которого строится вид, называется просто и бесхитростно — <code>Zend_View</code>. Вид позволяет отделить код, отвечающий за отображение страницы, от кода функций-действий.
Простейший вариант использования <code>Zend_View</code> выглядит как показано ниже:
<source lang="php">$view = new Zend_View();
$view->setScriptPath('/path/to/view_files');
echo $view->render('view.php');</source>
Легко можно понять, что если бы мы захотели поместить приведенный код в каждую функцию-действие, это привело бы к его многократному дублированию, что не явилось бы красивым решением. Вместо этого, мы выполним инициализацию вида отдельно, а из функций-действий затем будем только обращаться к нему.
Разработчики Zend Framework предусмотрели решение этой задачи и для ее решения создали так называемые «помощники действий» (action helpers). Класс <code>Zend_Controller_Action_Helper_ViewRenderer</code> инициализирует свойство <code>view</code> для нашего последующего использования (<code>$this->view</code>), а так же занимается отображением скриптов вида. Для рендеринга веб-страниц он использует объект <code>Zend_View</code>, который в свою очередь находит скрипты видов в файлах <code>views/scripts/{имя_контроллера}/{имя_действия}.phtml</code> (по-умолчанию они хранятся именно там, но при необходимости дислокацию скриптов можно менять).
После рендеринга, сгенерированный контент веб-страницы сохраняется в объекте Response, который, как было упомянуто ранее, служит для группировки HTTP заголовков, содержимого страницы и сгенерированных в ходе работы скрипта исключительных ситуаций. в итоге, первичный контроллер автоматически отсылает веб-клиенту заголовок страницы и ее содержательную часть.
Итак, для того, чтобы добавить вид в наше приложение, все, что нам потребуется, — создать несколько файлов с тестовым кодом. Никаких принципиальных изменений внутри контроллера не понадобится, но для большей наглядности мы выполним небольшую корректировку выводимого им текста.
Изменения внутри класса <code>IndexController</code> выделены жирным шрифтом.
<code>zf-tutorial/application/contollers/IndexController.php:</code>
<source lang="php"><?php
class IndexController extends Zend_Controller_Action
{
function indexAction()
{
$this->view->title = "My Albums";
}
function addAction()
{
$this->view->title = "Add New Album";
}
function editAction()
{
$this->view->title = "Edit Album";
}
function deleteAction()
{
$this->view->title = "Delete Album";
}
}</source>
Внутри каждой функции мы присваиваем значение переменной <code>title</code> свойства <code>view</code>. Заметьте, что при этом в действительности не происходит никакого отображения текста — это задача первичного контроллера, которая будет выполнена в самом конце работы системы.
Теперь нам понадобится добавить четыре файла для видов нашего веб-приложения.
<code>zf-tutorial/application/views/scripts/index/index.phtml:</code>
<source lang="php"><html>
<head>
<title><?php echo $this->escape($this->title); ?></title>
</head>
<body>
<h1><?php echo $this->escape($this->title); ?></h1>
</body>
</html></source>
<code>zf-tutorial/application/views/scripts/index/add.phtml:</code>
<source lang="php"><html>
<head>
<title><?php echo $this->escape($this->title); ?></title>
</head>
<body>
<h1><?php echo $this->escape($this->title); ?></h1>
</body>
</html></source>
<code>zf-tutorial/application/views/scripts/index/edit.phtml:</code>
<source lang="php"><html>
<head>
<title><?php echo $this->escape($this->title); ?></title>
</head>
<body>
<h1><?php echo $this->escape($this->title); ?></h1>
</body>
</html></source>
<code>zf-tutorial/application/views/scripts/index/delete.phtml:</code>
<source lang="php"><html>
<head>
<title><?php echo $this->escape($this->title); ?></title>
</head>
<body>
<h1><?php echo $this->escape($this->title); ?></h1>
</body>
</html></source>
При тестирование каждого из действий, должны быть отображены выделенные заголовки каждой из страниц.
<h5>Общий HTML-код</h5>
Уже сейчас становится заметно использование одинаковых фрагментов HTML кода в наших видах. Выделим идентичные для всех файлов части в два отдельных файла: <code>header.phtml</code> и <code>footer.phtml</code>, созданных внутри все той же директории со скриптами.
<code>zf-tutorial/application/views/scripts/header.phtml:</code>
<source lang="html"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title><?php echo $this->escape($this->title); ?></title>
</head>
<body>
<div id="content"></source>
(Обратите внимание на изменения, добавленный в код.)
<code>zf-tutorial/application/views/scripts/footer.phtml:</code>
<source lang="html"> </div>
</body>
</html></source>
Файлы видов так же понадобится откорректировать.
<code>zf-tutorial/application/views/scripts/index/index.phtml:</code>
<source lang="php"><?php echo $this->render('header.phtml'); ?>
<h1><?php echo $this->escape($this->title); ?></h1>
<?php echo $this->render('footer.phtml'); ?></source>
<code>zf-tutorial/application/views/scripts/index/add.phtml:</code>
<source lang="php"><?php echo $this->render('header.phtml'); ?>
<h1><?php echo $this->escape($this->title); ?></h1>
<?php echo $this->render('footer.phtml'); ?></source>
<code>zf-tutorial/application/views/scripts/index/edit.phtml:</code>
<source lang="php"><?php echo $this->render('header.phtml'); ?>
<h1><?php echo $this->escape($this->title); ?></h1>
<?php echo $this->render('footer.phtml'); ?></source>
<code>zf-tutorial/application/views/scripts/index/delete.phtml:</code>
<source lang="php"><?php echo $this->render('header.phtml'); ?>
<h1><?php echo $this->escape($this->title); ?></h1>
<?php echo $this->render('footer.phtml'); ?></source>
<h5>Стилевое оформление</h5>
Для того, чтобы сделать наше веб-приложение визуально более привлекательным, используем CSS. На этом этапе возникает небольшая проблема, связанная с тем, что заранее неизвестен URL корневой директории, необходимый для ссылки на CSS-файл. Для разрешения этой задачи используется функция <code>getBaseUrl()</code> передаваемого в вид объекта <code>Request</code>. Она и предоставит нам неизвестный заранее фрагмент URL.
Это свойство становится доступным из любого действия, после выполнения <code>IndexController::init()</code>.
<code>zf-tutorial/application/controllers/IndexController.php:</code>
<source lang="php">...
class IndexController extends Zend_Controller_Action
{
function init()
{
$this->view->baseUrl = $this->_request->getBaseUrl();
}
function indexAction()
{
...</source>
Ссылку на CSS-фалй понадобится добавить в секцию <head> файла <code>header.phtml</code>:
<code>zf-tutorial/application/views/scripts/header.phtml:</code>
<source lang="php">...
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title><?php echo $this->escape($this->title); ?></title>
<link rel="stylesheet" type="text/css" media="screen"
href="<?php echo $this->baseUrl;?>/public/styles/site.css" />
</head>
...</source>
В завершении, понадобится создать, собственно, сам стилевой файл.
<code>zf-tutorial/public/styles/site.css:</code>
<source lang="css">body,html {
font-size: 100%;
margin: 0;
font-family: Verdana, Arial, Helvetica, sans-serif;
color: #000;
background-color: #fff;
}
h1 {
font-size:1.4em;
color: #800000;
background-color: transparent;
}
#content {
width: 770px;
margin: 0 auto;
}
label {
width: 100px;
display: block;
float: left;
}
#formbutton {
margin-left: 100px;
}
a {
color: #800000;
}</source>
Это заставит страницы выглядеть несколько приятнее.
<a href="http://habrahabr.ru/post/31173/">Продолжение статьи</a>
Продолжаем рассказ о Zend Framework. В <a href="http://habrahabr.ru/post/31168/">первой части</a> статьи была описана концепция программной архитектуры MVC, рассмотрена структура типового веб-приложения, базирующегося на Zend Framework и выполнена демонстрационная реализация контроллера и вида на его основе. Во второй части будет раскрыта тема модели и приведен пример взаимодействия приложения с базой данных. <habracut>
<b>Автор:</b> Роб Ален, http://akrabat.com
<b>Оригинал:</b> http://akrabat.com/zend-framework-tutorial/
<b>Перевод:</b> Александр Мусаев, http://musayev.com
<h4>База данных</h4>
Теперь, когда управляющая часть нашего приложения и код визуализации разделены, пришло время заняться моделью. Запомните, что модель — базовая часть приложения, которая реализует его основные функции. Следовательно, в нашем случае модель выполняет работу с базой данных. Мы используем класс Zend Framework <code>Zend_Db_Table</code>, предназначенный для поиска, вставки, обновления и удаления записей в таблице базы данных.
<h4>Настройка</h4>
Для использования <code>Zend_Db_Table</code>, понадобится сообщить ему, к какой базе данных, под каким именем пользователя и с каким паролем он будет обращаться. Принимая во внимание, что эту информацию предпочтительно не забивать в код, воспользуемся конфигурационным файлом для ее хранения.
Zend Framework предоставляет для этой цели класс <code>Zend_Config</code>, обеспечивающий гибкий объектно-ориентированный доступ к конфигурационным файлам в форматах INI и XML. Остановим свой выбор на INI-файле:
<code>zf-tutorial/application/config.ini:</code>
<source>[general]
db.adapter = PDO_MYSQL
db.config.host = localhost
db.config.username = rob
db.config.password = 123456
db.config.dbname = zftest</source>
(Безусловно, вам понадобится использовать свои собственные параметры доступа к БД, а не приведенные в примере.)
Использовать <code>Zend_Config</code> будет очень просто:
<source lang="php">$config = new Zend_Config_Ini('config.ini', 'section');</source>
В данном случае, <code>Zend_Config_Ini</code> загружает одну секцию из INI-файла (таким же образом при необходимости можно загрузить любую другую секцию). Возможность именования секций реализована для того, чтобы лишние данные без нужды не загружались. <code>Zend_Config_Ini</code> использует точку в именах параметров в качестве иерархического разделителя, благодаря чему можно группировать родственные параметры. В нашем файле <code>config.ini</code>, параметры <code>host</code>, <code>username</code>, <code>password</code> и <code>dbname</code> будут сгруппированы в объекте <code>$config->db->config</code>.
Мы будем загружать конфигурационный файл из файла начальной загрузки (<code>index.php</code>):
<code>zf-tutorial/index.php:</code>
<source lang="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
...</source>
После загрузки необходимых для работы классов (<code>Zend_Config_Ini</code> и <code>Zend_Registry</code>), в объект <code>$config</code> загружается секция конфигурационного файла <code>application/config.ini</code> под именем <code>general</code>. Далее объект <code>$config</code> включается в реестр, что обеспечивая доступ к нему из всего приложения.
<b>Замечание:</b> В данном примере нет реальной потребности хранить <code>$config</code> в реестре. Это сделано в качестве примера работы «настоящего» приложения, в конфигурационном файле которого вам, вероятно, придется хранить нечто большее, чем параметры доступа к БД. При использовании реестра, обратите так же внимание, что данные в нем доступны на глобальном уровне, и что при неосторожном использовании это может стать причиной потенциальных конфликтных ситуаций внутри приложения.
<h5>Использование Zend_Db_Table</h5>
Для того, чтобы использовать класс <code>Zend_Db_Table</code>, нам понадобится передать в него параметра доступа к базе данных, которые были только что загружены. Для этого необходимо создать объект <code>Zend_Db</code> и зарегистрировать его функцией <code>Zend_Db_Table::setDefaultAdapter()</code>. Ниже приведен соответствующий фрагмент кода в файле первичной загрузки:
<code>zf-tutorial/index.php:</code>
<source lang="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
...</source>
<h5>Создание таблицы</h5>
Мы будем использовать базу данных MySQL, поэтому SQL запрос на создание таблицы будет выглядеть так:
<source lang="sql">CREATE TABLE album (
id int(11) NOT NULL auto_increment,
artist varchar(100) NOT NULL,
title varchar(100) NOT NULL,
PRIMARY KEY (id)
);</source>
Этот запрос можно выполнить через любой MySQL-клиент, например, phpMyAdmin или стандартную консольную утилиту.
<h5>Добавление тестовой записи</h5>
Добавим несколько тестовых записей в таблицу для проверки функциональности главной страницы, на которой они в последствии должны отображаться.
<source lang="sql">INSERT INTO album (artist, title)
VALUES
('James Morrison', 'Undiscovered'),
('Snow Patrol', 'Eyes Open');</source>
<h4>Модель</h4>
<code>Zend_Db_Table</code> — абстрактный класс, поэтому на его основе необходимо создать специализированный на нашей задаче класс-наследник. Имя нового класса не имеет принципиального значения, но такие классы стоит называть аналогично соответствующим им таблицам БД (это повысит уровень самодокументируемости кода). Таким образом, для нашей таблицы <code>album</code> имя класса будет <code>Album</code>.
Для того, чтобы передать в <code>Zend_Db_Table</code> имя таблицы, которой он будет управлять, необходимо присвоить это значение защищенному свойству класса <code>$_name</code>. Стоит отметить, что в классе <code>Zend_Db_Table</code> по-умолчанию всегда используется ключевое автоинкрементное поле таблицы с именем <code>id</code>. При необходимости значение имени этого поля так же можно менять.
Класс <code>Album</code> будет храниться в директории моделей.
<code>zf-tutorial/application/models/Album.php:</code>
<source lang="php"><?php
class Album extends Zend_Db_Table
{
protected $_name = 'album';
}</source>
Ничего сложного, не так ли? Учитывая, что наши потребности в данном случае совсем невелики, базовой функциональности <code>Zend_Db_Table</code> хватит, чтобы полностью их удовлетворить. Если же в ваших задачах понадобится реализовать более сложные операции взаимодействия с базой данных, то класс модели — это именно то место, куда стоит поместить соответствующий код.
<h4>Список альбомов</h4>
Теперь, когда мы сконфигурировали базу данных, можно приступить к выполнению основной задачи приложения и отобразить несколько записей. Это должно быть реализовано в классе <code>IndexController</code>. Каждая функция-действие внутри <code>IndexController</code> взаимодействует с таблицей <code>album</code> через класс <code>Album</code>. Поэтому имеет смысл загрузить его при инициализации контролера внутри функции <code>init()</code>.
<code>zf-tutorial/application/controllers/IndexController.php:</code>
<source lang="php">...
function init()
{
$this->view->baseUrl = $this->_request->getBaseUrl();
Zend_Loader::loadClass('Album');
}
...</source>
<b>Замечание:</b> В данном фрагменте кода приведен пример использования метода <code>Zend_Loader::loadClass()</code> для загрузки нестандартных классов. Это срабатывает, т. к. директория моделей (в которой хранится подгружаемый класс <code>Albums</code>) была добавлена нами в <code>include_path</code>.
Создадим список альбомов из базы с помощью <code>indexAction()</code>:
<code>zf-tutorial/application/controllers/IndexController.php:</code>
<source lang="php">...
function indexAction()
{
$this->view->title = "My Albums";
$album = new Album();
$this->view->albums = $album->fetchAll();
}
...</source>
Функция <code>fetchAll()</code> возвращает объект <code>Zend_Db_Table_Rowset</code>, который позволит нам сформировать список из всех записей в шаблонном файле вида:
<code>zf-tutorial/application/views/scripts/index/index.phtml:</code>
<source lang="php"><?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'); ?></source>
По адресу <code>http://localhost/zf-tutorial/</code> теперь должен отображаться список из двух наших альбомов.
<h4>Добавление нового альбома</h4>
Теперь перейдем к функции добавления нового диска в базу. Эта задача состоит из двух частей:
<ul>
<li>отображение формы, через которую пользователь будет вводить данные;</li>
<li>добавление принятых из формы данных в базу.</li>
</ul>
Реализуем перечисленные операции в функции-действии <code>addAction()</code>:
<code>zf-tutorial/application/controllers/IndexController.php:</code>
<source lang="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>
}
...</source>
Обратите внимание, как в начале функции было определено, имела ли место пересылка данных их формы. Если данные из формы были переданы, мы извлекаем значения <code>artist</code> и <code>title</code>, и обрабатываем их фильтром HTML тагов <code>Zend_Filter_StripTags</code>. Далее, если строки имеют непустое значение, мы используем класс <code>Album()</code> для добавления новой записи в таблицу БД. После добавления альбома, пользователь переадресуется обратно на главную страницу методом контроллера <code>_redirect()</code>.
Последнее, что понадобится сделать, — подготовить HTML-форму для шаблона вида. Забегая вперед, можно отметить, что форма редактирования записей будет выглядеть идентично форме для их добавления. Поэтому мы используем общий шаблонный файл (<code>_form.html</code>), который будет использован в <code>add.phtml</code> и <code>edit.phtml</code>:
Шаблон страницы добавления нового альбома:
<code>zf-tutorial/application/views/scripts/index/add.phtml:</code>
<source lang="php"><?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'); ?></source>
<code>zf-tutorial/application/views/scripts/index/_form.phtml:</code>
<source lang="php"><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></source>
Получился достаточно несложный код. Учитывая, что мы предполагаем применять <code>_form.phtml</code> еще и при редактировании записей, используем переменную <code>$this->action</code>, вместо того, чтобы жестко задавать имя необходимого действия. Таким же образом, через переменную задается надпись на кнопке передачи данных из формы.
<h4>Редактирование альбома</h4>
Редактирование альбома во многом идентично добавлению новой записи, поэтому и код получается похожим:
<code>zf-tutorial/application/controllers/IndexController.php:</code>
<source lang="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';
}
...</source>
Стоит отметить, что в тех случаях, когда не была выполнена передача данных в скрипт из формы, мы можем получить параметр <code>id</code> из свойства <code>params</code> объекта <code>Request</code>, с помощью метода <code>getParam()</code>.
Код шаблона приведен ниже:
<code>zf-tutorial/application/views/scripts/index/edit.phtml:</code>
<source lang="php"><?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'); ?></source>
<h4>Рефакторинг</h4>
Безусловно, от вашего внимания не могло ускользнуть, что <code>addAction()</code> и <code>editAction()</code> очень похожи, а шаблоны добавления и редактировании записей вообще идентичны. Логично предположить необходимость рефакторинга кода. Решите эту задачу самостоятельно, в качестве дополнительного практического упражнения.
<h4>Удаление альбома</h4>
Для того, чтобы завершить разработку приложения, понадобится добавить функцию удаления записей. У нас предусмотрены ссылки для удаления альбомов напротив каждого элемента в списке. Первое приходящее на ум решение — удалять записи сразу при клике по одной из этих ссылок. Но это неправильный подход. Вспомните, что согласно спецификации HTTP, не следует выполнять необратимых действий с помощью метода GET. В таких случаях рекомендуется применять POST. Примером того может быть работа Google Accelerator.
Мы должны запрашивать подтверждение перед удалением записей и удалять их только после того, как оно будет получено. Код, реализующий эти действия, в некоторой степени похож на уже существующие в нашем приложении функции-действия:
<code>zf-tutorial/application/controllers/IndexController.php:</code>
<source lang="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('/');
}
...</source>
Использован тот же способ определения метода обращения к скрипту и выбора требуемой операции (удаления записи или выдачи формы подтверждения). Точно так же, как в случае с добавлением и редактированием, удаление выполняется через <code>Zend_Db_Table</code> методом <code>delete()</code>. В конце функции выполняется перенаправление пользователя на страницу со списком альбомов. Таким образом, если одна из проверок корректности параметров не пройдена, происходит возврат на исходную страницу без помощи многократного обращения к <code>_redirect()</code> внутри функции.
Шаблон представляет собой простую форму:
<code>zf-tutorial/application/views/scripts/index/delete.phtml:</code>
<source lang="php"><?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'); ?></source>
<h4>Устранение неполадок</h4>
Если возникают сложности при обращении к любым действиям помимо <code>index</code>/<code>index</code>, скорее всего причина состоит в том, что класс-роутер не может корректно определить, в какой директории находится ваш веб-сайт. Такая ситуация может возникнуть, если URL сайта не соответствует пути к его директории относительно корневого каталога, открытого для доступа из сети.
Если приведенный выше код не соответствует вашему случаю, необходимо задать переменной <code>$baseURL</code> корректное для вашего сервера значение:
<code>zf-tutorial/index.php:</code>
<source lang="php">...
// setup controller
$frontController = Zend_Controller_Front::getInstance();
$frontController->throwExceptions(true);
$frontController->setBaseUrl('/mysubdir/zf-tutorial');
$frontController->setControllerDirectory('./application/controllers');
...</source>
Значение <code>/mysubdir/zf-tutorial/</code> понадобится заменить на действительный путь к файлу <code>index.php</code>. Например, если ваш URL к <code>index.php</code> выглядит как <code>http://localhost/~ralle/zf-tutorial/index.php</code>, корректным значением для переменной <code>$baseUrl</code> будет <code>/~ralle/zf-tutorial/</code>.
<h4>Заключение</h4>
На этом построение простого но полнофункционального MVC-приложения можно считать завершенным. Надеюсь, этот обзор был полезен и информативен для вас. Любые замечания к оригинальному тексту можно отправлять автору статьи по адресу rob@akrabat.com. Комментарии, связанные с русским переводом, отправляйте на musayev@yandex.ru.
В данной статье выполнен лишь поверхностный обзор Zend Framework, в котором существует великое множество других классов, помимо перечисленных здесь. Для подробного ознакомления с ними следует обратиться к соответствующим ресурсам:
<ul>
<li>Официальная документация → http://framework.zend.com/manual</li>
<li>Wiki с дополнительными материалами → http://framework.zend.com/wiki</li>
<li>Wiki для разработчиков → http://framework.zend.com/developer</li>
<li>Оригинал статьи на английском языке, и примеры кода на сайте автора → http://akrabat.com/zend-framework-tutorial/</li>
</ul>
При переводе использовались материалы свободной энциклопедии Wikipedia.org.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment