Мы пишем программы, чтобы облегчить жизнь рядовым пользователям. Однако какой бы уникальной ни была ваша программа, не последнюю роль играет ее внешний вид, т.е. графический интерфейс. Дизайнеры и специалисты по построению 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.

Leave a Reply

3 thoughts on “Zend Layout и ajax приложение просмотра погоды”
  1. Если внимательно посмотреть на возвращаемые результаты в формате XML, то [b]бует[/b] видно, что сервис возвращает данные, [b]которое[/b] содержатся в атрибутах специфичных тэгов (TEMPERATURE, PRESSURE и т.д.), поэтому для доступа к этим атрибутам мы используем конструкцию вида

  2. Как “указывать свои собственные переменные, которые отвечают за навигацию, меню, либо блок какой-либо информации”?

    1. Не совсем понял вопрос. Поясните, пожалуйста, что хотите сделать.

Leave a Reply

Your email address will not be published. Required fields are marked *