Мы пишем программы, чтобы облегчить жизнь рядовым пользователям. Однако какой бы уникальной ни была ваша программа, не последнюю роль играет ее внешний вид, т.е. графический интерфейс. Дизайнеры и специалисты по построению UI (user interface) тратят много сил, чтобы создать понятную и удобную “обертку” для программы. Однако за программистами остается
работа по созданию инструмента, который бы позволил легко настраивать внешний вид, и не последнюю роль в этом процессе играют шаблоны. Поэтому очень важно использовать простые и
понятные методики использования шаблонов, чтобы другие люди с легкостью могли настраивать внешний вид как они сами того пожелают. Zend Framework предлагает два компонента
для управления представлением программы – Zend_View и Zend_Layout. Сегодня мы рассмотрим лишь один из них, а именно Zend_Layout.
Zend_Layout является своего рода макетом страницы, он представляет собой каркас, фундамент вашего представления. Именно в своем layout вы определяете доктайп, стили и внешние подключаемые файлы в секции head, разбиваете страницу на различные блоки, чтобы потом можно было отрендерить их, используя вспомогательные шаблоны или снипеты. Таким образом ваша HTML страница изначально будет строиться с использованием макета layout. Zend_Layout реализует паттерн двухшаговая шаблонизация. Т.е. сначала вы представляете данные в виде некой логической структуры, а затем используете необходимое форматирование данных, чтобы представить их пользователю. По умолчанию Zend Framework предлагает хранить ваши макеты в папке layouts/scripts, которая находится в папке application вашего приложения.
Основы использования Zend_Layout
Возможно, вы заметили, что в самом начале, когда мы воспользовались утилитой zf для создания нашего первого приложения на Zend Framework, исходный код не являлся валидной HTML страницей, т.е. отсутствовали тэги html и body в частности. Мы ведь не указали свой собственный макет для страницы, а использовали лишь снипет index.phtml. Так как же Zend Framework может узнать, где искать макеты для страниц?
Это можно сделать несколькими способами:
a) Создать массив с настройками, содержащими путь и названия макета, а затем передать этот массив объекту Zend_Layout
$options = array(
'layout' => 'foo',
'layoutPath' => '/path/to/layouts',
'contentKey' => 'CONTENT',
);
$layout = new Zend_Layout($options);
// или так:
$layout = Zend_Layout::startMvc($options);
б) Использовать конфигурационный файл
// layout.ini
[layout]
layout = "foo"
layoutPath = "/path/to/layouts"
contentKey = "CONTENT"
$config = new Zend_Config_Ini('/path/to/layout.ini', 'layout');
// передаем конструктору объект Zend_Config:
$layout = new Zend_Layout($config);
// можно сделать и так:
$layout = Zend_Layout::startMvc($config);
в) Использовать аксессоры
$layout->setLayout('foo')
->setLayoutPath('/path/to/layouts')
->setContentKey('CONTENT');
Zend_Layout и application.ini
Я предпочитаю указывать макеты в конфигурационных файлах. Конфигурационные файлы по умолчанию находятся в папке application/configs.
В индексном файле, если помните, мы определили application.ini и передали его в качестве параметра конструктору Zend_Application. Теперь нам необходимо добавить в конфигурационный файл следующие строчки:
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
resources.layout.layout = "layout"
В итоге application.ini примет следующий вид:
[production]
phpSettings.display_startup_errors = 0
phpSettings.display_errors = 0
includePaths.library = APPLICATION_PATH "/../library"
bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"
resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
resources.layout.layout = "layout"
[staging : production]
[testing : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
[development : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
Теперь загрузчик Zend Framework знает, что мы хотим использовать папку layouts/scripts для своих макетов, и что макет layout.phtml будет нашим основным макетом.
Создадим макет layout.phtml в папке application/layouts/scripts нашего приложения:
<html>
<head>
<title>
My Zend Framework Application
</title>
</head>
<body>
<?=$this->layout()->content;?>
</body>
</html>
Конструкция $this->layout() возвращает объект слоя, а content является просто переменной по умолчанию, которая включает в себя содержимое выводимого блока. В дальнейшем вы будете указывать свои собственные переменные, которые отвечают за навигацию, меню, либо блок какой-либо информации, но пока у нас есть лишь одна переменная content.
Сохраним файл и откроем индексную страницу нашего проекта в браузере. Вроде бы ничего не изменилось, однако если посмотреть на исходный код страницы, то мы увидим то, что мы только что прописали в файле layout.phtml.
Доступ к объекту layout.
Теперь, создавая свои шаблоны в папке views, вместо
<?=$this->layout()->content;?>
будет подставляться содержимое ваших шаблонов. Попробуйте создать какой-нибудь контроллер со своим шаблоном для действия и убедиться в этом.
Доступ к объекту слоя посредством
$this->layout()
является не единственным способом. Доступ можно получить через контроллер
$layout = $this->_helper->layout();
используя статический метод getMvcInstance()
$layout = Zend_Layout::getMvcInstance();
через загрузчик
$layout = $bootstrap->getResource('Layout');
AJAX приложение просмотра погоды
Теперь когда мы знаем как включать макеты в конечное представление страницы, мы научимся их отключать ;). Забегая вперед, скажу, что это реализуется методом disableLayout().
В большинстве случаев, наверно, нам придется создавать различные страницы (новости, фотогалереи, каталог и пр.), поэтому так или иначе нам придется работать с макетами. Однако макет может оказаться ненужным, когда мы имеем дело с ajax приложениями. Ведь часто посылают лишь запрос контроллеру, а в ответе нужно получить данные без всякого форматирования страницы.
Вот здесь и может понадобиться решение отказаться от макетов. Но как это сделать, если все запросы транслируются индексному файлу, тот подключает загрузчик, а последний в свою очередь запускает все приложение, инициализируя Zend_Layout?
Рассмотрим на примере, как этого можно добиться.
Итак создадим приложение, которое будет выводить погоду нескольких городов у нас на странице.
Откроем наш макет layout.phtml и подключим jquery, который будем использовать для облегчения написания ajax приложения.
Добавьте в секцию head следующий код:
<?php
$this->headScript()->appendFile('http://www.google.com/jsapi');
echo $this->headScript();
?>
<script type="text/javascript">
google.load("jquery", "1.3.1");
</script>
Здесь мы воспользовались сервисом от гугла jsapi, загрузили jquery версии 1.3.1. Можно просто скачать jquery, положить в какую-нибудь папку и подключить его аналогичным образом.
Далее напишем функцию, которая будет посылать запрос другому скрипту, обрабатывать ответ и выводить результаты.
В IndexController добавим действие search, где запрос будет перенаправляться gismeteo, чтобы узнать погоду выбранного города, а затем будет выводиться результат.
На странице http://informer.gismeteo.ru/getcode/xml.php?id=27612 дано описание формата для выдачи. Выберем три города: Москва, Берлин, Вашингтон. В строке запросе меняться
будут только идентификаторы выбранных городов, поэтому наш селект бокс будет выглядеть следующим образом:
<select name="q" id="q">
<option value="27612">Москва</option>
<option value="10381">Берлин</option>
<option value="72403">Вашингтон</option>
</select>
Если вам понадобятся другие города, просто добавьте их, указав соответствующие идентификаторы, которые можно посмотреть на странице gismeteo.
Итак, у нас есть три города, погоду для которых мы хотим узнать, формат выдачи данных о погоде. Теперь осталось написать запрос к сайту gismeteo, чтобы тот вернул нам
запрашиваемые данные.
Запрос к сервису с погодой.
В файл views/scripts/index/index.phtml добавим такой код:
<select name="q" id="q">
<option value="27612">Москва</option>
<option value="10381">Берлин</option>
<option value="72403">Вашингтон</option>
</select>
<input type="button" id="search" value="search" onclick="search()"/>
<dl id="search-results">
</dl>
<script type="text/javascript">
function search() {
$.post("/search", {q: $("#q").val()}, function (response) {
// process data
var forecast = response.MMWEATHER.REPORT.TOWN.FORECAST;
for (var i = 0; i < forecast.length; i++) {
$("#search-results").append("<dt>" + forecast[i]["@attributes"].day +
"." + forecast[i]["@attributes"].month +
"." + forecast[i]["@attributes"].year +
", Hour: " + forecast[i]["@attributes"].hour + "</dt>");
$("#search-results").append("<dd>давление: max " + forecast[i].PRESSURE["@attributes"].max +
", min: " + forecast[i].PRESSURE["@attributes"].min +
" : температура - max: " + forecast[i].TEMPERATURE["@attributes"].max +
" min: " + forecast[i].TEMPERATURE["@attributes"].min + "</dd>");
}
}, 'json');
}
</script>
У нас имеется селект бокс, кнопка поиска и поле вывода результатов. Сама функция поиска посылает запрос на адрес /search, получает данные в json формате и выводит их в указанном месте.
Если внимательно посмотреть на возвращаемые результаты в формате XML, то бует видно, что сервис возвращает данные, которое содержатся в атрибутах специфичных тэгов (TEMPERATURE, PRESSURE и т.д.), поэтому для доступа к этим атрибутам мы используем конструкцию вида
<pre>forecast[i]["@attributes"]</pre>
Это становится возможным, так как мы воспользуемся компонентом Zend_Json, но об этом чуть ниже.
Однако если сейчас попытаться нажать на кнопку, то можно увидеть (если вы используете firebug или аналогичный отладчик), что запрос вернет 404 ошибку, ведь страницы /search у нас еще не существует.
Мы не будем создавать дополнительный контроллер search, вместо этого в индексном контроллере добавим еще одно действие searchAction(). Для того чтобы запрос был обработан этим действием нам необходимо указать маршрут посредством Zend_Controller_Router_Route. Вы можете посмотреть примеры стандартного маршрутизатора для более ясной картины, как это
работает. Сейчас же мы просто добавим маршрут в метод _initRoutes() нашего загрузчика. Добавляем _initRoutes() в application/Bootstrap.php:
protected function _initRoutes()
{
$router = Zend_Controller_Front::getInstance()->getRouter();
$router->addRoute(
'search',
new Zend_Controller_Router_Route("/search", array(
"controller" =>
"index",
"action" => "search"
)
)
);
}
Теперь если перейти на страницу /search будет вызвано действие search контроллера index. Последнее что нам осталось, это реализовать действие searchAction в индексном контроллере.
Вот здесь нам как раз и понадобится отключить layout для страницы, вы ведь еще не забыли про disableLayout()? 😉
Ведь если этого не сделать нам просто придет ответ в виде HTML страницы, а нам надо получить лишь данные в формате JSON. Нам также не нужно рендерить шаблоны.
Все это делается двумя строчками кода:
public function searchAction()
{
$this->_helper->viewRenderer->setNoRender();
$this->_helper->getHelper("layout")->disableLayout();
}
Осталось лишь отправить запрос сервису с погодой. В Zend Framework есть очень удобный компонент для работы с HTTP запросами Zend_Http_Client. Его-то мы и будем использовать для получения данных с удаленного сайта. Сейчас я не буду рассматривать работу этого компонента, все что нам нужно – это три метода setUri(), request(), getBody():
$q = $this->_getParam("q");
$client = new Zend_Http_Client();
$client->setUri("http://informer.gismeteo.ru/xml/" . $q . "_1.xml");
echo $client->request("GET")->getBody();
Параметр q приходит к нам в запросе, который посылает функция search(). Далее лишь создается объект Zend_Http_Client и посылается запрос на указанный URL; больше нам от этого объекта ничего не понадобится 😉
Если все оставить как есть, то мы получим данные в XML формате, можно, конечно, и так поступить, но мне лично удобнее работать с JSON, поэтому мы сконвертируем XML в JSON при помощи Zend_JSON. В итоге наш метод searchAction() примет окончательный вид:
public function searchAction()
{
$this->_helper->viewRenderer->setNoRender();
$this->_helper->getHelper("layout")->disableLayout();
$q = $this->_getParam("q");
$client = new Zend_Http_Client();
$client->setUri("http://informer.gismeteo.ru/xml/" . $q . "_1.xml");
$jsContents = Zend_Json::fromXml($client->request("GET")->getBody(), false);
echo $jsContents;
}
Теперь все готово, и если нажать на кнопку поиска, то, подождав, вы получите погоду для выбранного города. Добавим небольшое украшение, крутящийся кружочек во времся аякс-запроса, чтобы придать большую интерактивность нашему приложению.
Для генерации различных изображений ajax загрузки я использую сервис ajaxload.info.
В шаблон index.phtml добавим span бокс, где будет выводиться картинка ожидания,
<span id="wait-box"></span>
и саму картинку в секцию javascript
var ajaxImg = new Image();
ajaxImg.src = '/images/ajax-loader.gif';
При нажатии на кнопку будет показываться картинка ожидания, а когда будет получен ответ от удаленного сервера, эту картинку скроем. Ну, и добавим название города для которого будет выводиться погода в результат вывода:
function search() {
$("#wait-box").append(ajaxImg);
var city = $("#q option:selected").text();
$.post("/search", {q: $("#q").val()}, function (response) {
// process data
var forecast = response.MMWEATHER.REPORT.TOWN.FORECAST;
for (var i = 0; i < forecast.length; i++) {
$("#search-results").append("<dt><b>Погода для г. " + city + "</b> на " + forecast[i]["@attributes"].day +
"." + forecast[i]["@attributes"].month +
"." + forecast[i]["@attributes"].year +
", Hour: " + forecast[i]["@attributes"].hour + "</dt>");
$("#search-results").append("<dd>давление: max " + forecast[i].PRESSURE["@attributes"].max +
", min: " + forecast[i].PRESSURE["@attributes"].min +
" : температура - max: " + forecast[i].TEMPERATURE["@attributes"].max +
" min: " + forecast[i].TEMPERATURE["@attributes"].min + "</dd>");
}
$("#wait-box").empty();
}, 'json');
}
Вот и все, наше приложения оказалось не таким уж и сложным, а результат можно увидеть на картинке 😉

Скачать весь проект можно по следующей ссылке zf.tar.
P.S. Закончен перевод 5-й главы документации Zend Framework Плагины в Zend Framework.
Если внимательно посмотреть на возвращаемые результаты в формате XML, то [b]бует[/b] видно, что сервис возвращает данные, [b]которое[/b] содержатся в атрибутах специфичных тэгов (TEMPERATURE, PRESSURE и т.д.), поэтому для доступа к этим атрибутам мы используем конструкцию вида
Как “указывать свои собственные переменные, которые отвечают за навигацию, меню, либо блок какой-либо информации”?
Не совсем понял вопрос. Поясните, пожалуйста, что хотите сделать.