Создание интерфейса REST API в Agavi

Реализуйте интерфейс REST API с помощью PHP-инфраструктуры Agavi

Agavi – это гибкая и масштабируемая open-source инфраструктура разработки приложений. Одной из ее ключевых особенностей является встроенная поддержка маршрутов REST, обеспечивающая быстрое добавление интерфейса REST API для сторонней разработки нового или существующего Web-приложения. В этой статье мы подробно рассмотрим эту возможность и покажем, как создать интерфейс REST API с поддержкой форматов XML и JSON.

Викрам Васвани (Vikram Vaswani), основатель компании, Melonfire

Викрам Васвани (Vikram Vaswani) – основатель и президент консалтинговой фирмы Melonfire, специализирующейся на технологиях и инструментах с открытым исходным кодом. Также является автором книг Решения по программированию на PHP and Как сделать все что угодно с помощью PHP и MySQL.



08.04.2011

Введение

Сегодня в каждом серьезном Web-приложении имеется интерфейс REST API. Он есть у Flickr, Google, Bit.ly, NetFlix, а также у многих других популярных приложений. Популярность REST в роли архитектурного шаблона обусловлена тем, что этот подход интуитивно сопоставляет существующим методам HTTP стандартные операции с данными, а также является облегченной альтернативой существующим архитектурам на базе SOAP и RPC, соответствуя всем требованиям, но требуя выполнения меньшей работы по заданию типов данных. Это благотворно сказывается на времени и стоимости разработки, поскольку API-интерфейсы на основе REST, как правило, быстрее реализовывать и проще использовать, чем их аналоги на базе SOAP и RPC.

Часто используемые сокращения

  • API: Application program interface – программный интерфейс приложения
  • CRUD: Create Read Update Delete – операции создания, чтения, обновления и удаления
  • DOM: Document Object Model – объектная модель документов
  • HTML: Hypertext Markup Language – язык гипертекстовой разметки
  • HTTP: Hypertext Transfer Protocol – протокол передачи гипертекста
  • JSON: JavaScript Object Notation – текстовый формат обмена данными, основанный на JavaScript
  • MVC: Model-View-Controller – модель "представление-контроллер"
  • OOP: Object-oriented programming – объектно-ориентированное программирование
  • ORM: Object-Relational Mapping – объектно-реляционное отображение
  • REST: Representational State Transfer – передача репрезентативного состояния
  • RPC: Remote procedure call – удаленный вызов процедуры
  • SQL: Structured Query Language – язык структурированных запросов
  • URL: Uniform Resource Locator – унифицированный указатель ресурса
  • WSDL: Web Services Description Language – язык описания Web-сервисов
  • XML: Extensible Markup Language – расширяемый язык разметки

В предыдущей серии статей я познакомил вас с MVC-инфраструктурой Agavi и продемонстрировал, как можно быстро и эффективно разрабатывать масштабируемые Web-приложения (обратитесь к разделу Ресурсы). Для этого в Agavi имеется ряд полезных вещей:

  • Усовершенствованная фильтрация и проверка достоверности входных данных
  • OOP-совместимая архитектура
  • Настраиваемая URL-маршрутизация
  • Расширяемое управление доступом на основе ролей

Кроме того, предлагаются две неоценимые для разработчика REST API функции: встроенная поддержка методов REST HTTP и поддержка нескольких выходных типов, таких как XML и JSON.

В этой статье я покажу вам процесс написания простого интерфейса REST API с помощью Agavi. Если у вас уже имеется приложение на базе Agavi, эта статья объяснит вам, как выгодно использовать существующие соглашения инфраструктуры и раскрыть сторонним разработчикам внутренние структуры вашего приложения. Если вы создаете новое приложение на базе REST, эта статья покажет, как можно сделать это проще и эффективнее с помощью Agavi.


Что такое REST

Прежде всего нужно сказать несколько слов о подходе REST, известном также как Representational State Transfer – передача репрезентативного состояния. REST отличается от SOAP тем, что он основан на ресурсах и действиях, а не на методах и типах данных. Ресурс – это просто URL-адрес, указывающий на объект или элемент, над которым совершаются действия – например, ресурсом является адрес /users или /photos, а действие – это один из четырех HTTP-методов: GET (получить), POST (создать), PUT (обновить) и DELETE (удалить).

Чтобы лучше разобраться в этом, давайте рассмотрим простой пример. Предположим, у вас есть сервис по обмену и хранению фотографий, и вам нужны API-методы, чтобы разработчики могли удаленно добавлять или загружать фотографии из хранилища этого приложения. В подходе SOAP в вашем распоряжении, как правило, имеются API-методы SOAP, такие как createPhoto() и getPhoto(), которые посылают запрос XML-формата, содержащий параметры фотографии в качестве входных данных, выполняют действия по созданию или получению фотографии и возвращают XML-ответ с информацией об успешном или неудачном их выполнении. Язык SOAP WSDL определяет формат блоков запросов и ответов, типы данных различных входных параметров и диапазон возможных ответных значений.

Подход REST намного проще. В этом случае вы публикуете URL-адрес конечной точки, такой как /photos, и анализируете HTTP-методы при обращении к этому URL, чтобы понять, какое действие совершится. Например, вы выполняете для ресурса /photos HTTP-запрос POST (создать), чтобы создать новую фотографию, или запрос GET (получить), чтобы получить список уже имеющихся фотографий. Такой подход прост для понимания, поскольку сопоставляет существующим HTTP-методам операции CRUD, и потребляет меньше ресурсов, поскольку не требует формального определения типов данных заголовков запроса/ответа.

Типовые REST-соглашения для URL-запросов и их значения приведены ниже:

  • GET /items: получить список элементов /items
  • GET /items/123: получить элемент #123
  • POST /items: создать новый элемент
  • PUT /items/123: обновить элемент #123
  • DELETE /items/123: удалить элемент #123

Agavi имеет встроенную поддержку этих REST-соглашений. Если вы читали предыдущие статьи этой серии, то вы уже знаете, что инфраструктура автоматически будет переводить запросы GET и POST в методы executeRead() и executeWrite(). Точно так же запросы PUT и DELETE будут автоматически переводиться в методы executeCreate() и executeRemove(). Следовательно, чтобы определить новый интерфейс REST API, нужно просто определить эти методы, запрограммировать для них нужный код и правильно выполнить маршрутизацию запросов к этим методам. Это как раз то, чем мы будем заниматься в оставшейся части этой статьи.


Настройка тестового приложения

Прежде чем перейти к реализации интерфейса REST API, я хочу сделать несколько замечаний. В этой статье будет предполагаться, что у вас есть рабочая среда (Apache, PHP и MySQL), и что вы знакомы с основами SQL и XML. Также я предполагаю, что вы хорошо знакомы с базовыми принципами разработки приложений в Agavi, понимаете взаимосвязь между действиями (actions), представлениями (views), моделями (models) и маршрутами (routes), а также знакомы с использованием моделей Doctrine в приложениях Agavi. Наконец, я предполагаю, что ваш Web-сервер Apache поддерживает функции виртуального хостинга и переписывания ссылок (URL rewriting), а также запросы PUT и DELETE. Если вы не знакомы с вышеперечисленными темами, вам следует прочитать серии вводных статей по Agavi (обратитесь к разделу Ресурсы), прежде чем приступить к изучению дальнейшего материала этой статьи.

Наше тестовое приложение представляет собой простую базу данных, содержащую названия книг и имена их авторов. Интерфейс REST API позволит сторонним разработчикам получать, добавлять, удалять и обновлять информацию о книгах в этой базе с помощью стандартных REST-соглашений. В основном в данной статье будут описываться запросы и ответы в XML-формате, однако в конце статьи содержится раздел, показывающий, как обрабатывать запросы и ответы JSON.

Шаг 1. Инициализация нового приложения

Прежде всего необходимо настроить приложение Agavi, которое будет испытательным стендом для разработки в этой статье. Для инициализации нового проекта используйте сценарий Agavi. Оставляйте все значения по умолчанию, за исключением тех, что выделены ниже:

shell> agavi project-wizard
Project name [New Agavi Project]: ExampleApp
Project prefix (used, for example, in the project base action) [Exampleapp]: ExampleApp
Should an Apache .htaccess file with rewrite rules be generated (y/n) [n]? y
...

По завершении выполнения сценария создайте в конфигурационном файле Apache новый виртуальный хост для вашего тестового приложения, например, http://example.localhost/, и введите этот адрес в адресной строке вашего Web-браузера. Вы должны увидеть страницу приветствия Agavi, как показано на рисунке 1.

Рисунок 1. Страница приветствия Agavi по умолчанию

Шаг 2. Добавление нового модуля и соответствующих действий

Для простоты я буду полагать, что все защищаемые вами действия располагаются в модуле, отличном от модуля Default. Вернитесь в командную строку и создайте новый модуль Books с помощью сценария Agavi, как показано ниже:

shell> agavi module-wizard
Module name: Books
Space-separated list of actions to create for Books: Index Book
Space-separated list of views to create for Index [Success]: Success
Space-separated list of views to create for Book [Success]: Success Error
...

Это те действия, к которым мы вскоре добавим методы REST.

На этом шаге также необходимо удалить модуль Welcome, в соответствии с рекомендациями руководства по Agavi.

shell> rm -rf app/modules/Welcome
shell> rm -rf app/pub/welcome

Шаг 3. Обновление таблицы маршрутизации приложения

Следующим шагом необходимо обновить таблицу маршрутизации приложения, расположенную в директории $ROOT/app/config/routing.xml, в соответствии со стандартными маршрутами REST, которые обсуждались в предыдущем разделе. Необходимые определения маршрутов приведены в листинге 1.

Листинг 1. Определения маршрутов REST
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>

      <!-- действие по умолчанию "/" -->
      <route name="index" pattern="^/$" module="Default" action="Index" />

      <!-- REST-маршруты -->
      <route name="books" pattern="^/books" module="Books">
        <route name=".index" pattern="^/$" action="Index" />
        <route name=".book" pattern="^/(id:\d+)$" action="Book" />
      </route>      

    </routes>
  </ae:configuration>
</ae:configurations>

Теперь у вас должна появиться возможность обращаться к только что созданным действиям с помощью маршрутов, определенных в листинге 1. Чтобы проверить это, введите в адресной строке Web-браузера адрес http://example.localhost/books/ и убедитесь, что вы видите нечто похожее на рисунок 2.

Рисунок 2. HTML-заглушка Agavi

Шаг 4. Инициализация модели и базы данных о книгах

Следующим шагом нужно инициализировать базу данных приложения. Для этого создайте таблицу MySQL, которая будет содержать записи о книгах, как показано ниже:

mysql> CREATE TABLE IF NOT EXISTS book (
    ->   id int(11) NOT NULL AUTO_INCREMENT,
    ->   title varchar(255) NOT NULL,
    ->   author varchar(255) NOT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.13 sec)

Добавьте в таблицу несколько записей:

mysql> INSERT INTO `book` (`id`, `title`, `author`) 
    -> VALUES (1, 'Wolf Hall', 'Hilary Mantel');
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO `book` (`id`, `title`, `author`) 
    -> VALUES (2, 'Prayers for Rain', 'Dennis Lehane');
Query OK, 1 row affected (0.08 sec)

После этого загрузите объектно-реляционный проектор (object relational mapper, ORM) Doctrine (обратитесь к разделу Ресурсы) и добавьте его библиотеки в директорию $ROOT/libs/doctrine. Также обновите настройки вашего приложения в файле $ROOT/app/config/settings.xml, чтобы включить поддержку базы данных, а затем обновите конфигурационный файл вашей БД, расположенный, как правило, в директории $ROOT/app/config/databases.xml, чтобы задействовать адаптер Doctrine в Agavi. В листинге 2 приведен пример содержимого файла конфигурации:

Листинг 2. Конфигурация ORM Doctrine
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/databases/1.0">
  <ae:configuration>
    <databases default="doctrine">    
      <database name="doctrine" class="AgaviDoctrineDatabase">
        <ae:parameter name="dsn">mysql://user:pass@localhost/example
        </ae:parameter>
        <ae:parameter name="load_models">%core.lib_dir%/model
        </ae:parameter>
      </database>      
    </databases>
  </ae:configuration>
</ae:configurations>

На этом этапе можно с помощью Doctrine генерировать модели для этих таблиц. Не забудьте вручную скопировать классы получившейся модели в директорию $ROOT/app/lib/model/.

shell> cp /tmp/models/Book.php app/lib/model/
shell> cp /tmp/models/generated/BaseBook.php app/lib/model/

Процесс интеграции Doctrine с Agavi и дальнейшего его использования для генерации моделей из таблиц БД был подробно рассмотрен в третьей части серии вводных статей по Agavi (обратитесь к разделу Ресурсы в конце статьи).

Шаг 5. Определение выходного типа XML

По умолчанию Agavi сконфигурирован для работы только с выходным типом HTML. Поскольку наш тестовый интерфейс REST API изначально будет поддерживать формат XML, необходимо определить этот выходной тип и соответствующие заголовки ответа и отметить его в качестве типа по умолчанию. Для этого отредактируйте конфигурационный файл выходных типов $ROOT/app/config/output_types.xml, как показано в листинге 3.

Листинг 3. Конфигурация выходного типа XML
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/output_types/1.0">
  <ae:configuration>
    <output_types default="xml">

      <output_type name="html">
      ...
      </output_type>

      <output_type name="xml">
        <ae:parameter name="http_headers">
          <ae:parameter name="Content-Type">text/xml; charset=UTF-8
          </ae:parameter>
        </ae:parameter>
      </output_type>
    </output_types>
  </ae:configuration> 
</ae:configurations>

Если у вас не получается выполнить какие-либо из вышеописанных шагов, помните, что все они рассмотрены в подробностях в первой части серии вводных статей по Agavi (обратитесь к разделу Ресурсы). Кроме того, вы можете загрузить архив, содержащий полный код тестового приложения, в разделе Загрузка.


Обработка запросов GET

Типичный интерфейс REST API должен поддерживать два типа запросов GET: первый – для получения списка ресурсов (GET /books/), а второй – для получения определенного ресурса (GET /books/123). Используя таблицу маршрутизации, которая обсуждалась в предыдущем разделе, Agavi автоматически будет переадресовывать эти запросы к методам Books_IndexAction::executeRead() и Books_BookAction::executeRead() соответственно.

Метод executeRead() обработчика IndexAction должен возвращать ответ на запрос GET /books/ с кодом состояния 200 (OK) и выводить список всех доступных записей о книгах. Код обработчика приведен в листинге 4:

Листинг 4. Обработчик IndexAction для запросов GET
<?php
class Books_IndexAction extends ExampleAppBooksBaseAction
{ 
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->from('Book b');
    $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY); 
    $this->setAttribute('result', $result);
    return 'Success';
  }
}
?>

Для выполнения запроса на получение записей из таблицы базы данных в листинге 4 используется Doctrine, а результат задается в качестве переменной-представления IndexSuccessView. Следующим шагом мы добавляем метод executeXml(), чтобы получить информацию в виде XML-документа. Код приведен в листинге 5:

Листинг 5. XML-представление IndexSuccess
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{ 
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']); 
    }
    return $xml->asXml();
  }
}
?>

Здесь нет ничего сложного. Метод executeXml() генерирует новый DOM-документ, создает корневой элемент, а затем посредством SimpleXML строит остальной фрагмент XML-дерева, заполняя его информацией из результирующего набора данных Doctrine.

Чтобы увидеть это в действии, введите в адресную строку Web-браузера адрес http://example.localhost/books. Вы должны получить результат, изображенный на рисунке 3 (посмотреть текстовую версию рисунка 3).

Рисунок 3. XML-ответ на запрос GET с выборкой всех книг

Аналогично, метод executeRead() обработчика BookAction должен возвращать ответ на запрос GET /books/{id} в виде XML-документа, содержащего информацию о запрашиваемой книге. Код обработчика приведен в листинге 6:

Листинг 6. Обработчик BookAction для запросов GET
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->from('Book b')
          ->addWhere('id = ?', $rd->getParameter('id'));
    $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY); 
    if (count($result) == 0) {
      return 'Error';  
    }
    $this->setAttribute('result', $result);
    return 'Success';
  }
}
?>

В листинге 7 представлен код соответствующего валидатора, а в листинге 8 – код соответствующего представления BookSuccess.

Листинг 7. Валидатор BookAction
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Books/config/validators.xml"
>
  <ae:configuration>    
    <validators>
      <validator class="number">
        <arguments>
          <argument>id</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators> 
  </ae:configuration>
</ae:configurations>
Листинг 8. XML-представление BookSuccess
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);      
    }
    return $xml->asXml();
  }   
}
?>

Чтобы увидеть это в действии, введите в адресную строку Web-браузера адрес http://example.localhost/books/1. Вы должны получить результат, изображенный на рисунке 4 (посмотреть текстовую версию рисунка 4).

Рисунок 4. XML-ответ на запрос GET с выборкой отдельной книги

В случае если указанный ресурс недоступен, будет полезно возвратить код 404 (Not Found). Это легко сделать, если переадресовать BookErrorView на представление по умолчанию Error404SuccessView, после чего обновить его с помощью метода executeXml(), возвращающего состояние 404 в теле сообщения. Код представления Error404SuccessView приведен в листинге 9.

Листинг 9. XML-представление Error404Success
<?php
class Default_Error404SuccessView extends ExampleAppDefaultBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $this->getResponse()->setHttpStatusCode('404');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('error'); 
    $dom->appendChild($root);
    $message = $dom->createElement('message', '404 Not Found');
    $root->appendChild($message);
    return $dom->saveXml();
  }
}
?>

Обработка запросов POST

Обработка запросов POST немного сложнее. Обычное REST-соглашение подразумевает, что запрос POST создает новый ресурс, содержащий в своем теле все необходимые входные данные (в нашем случае это имя автора и название книги) ресурса. На сегодняшний день Agavi может автоматически считывать и преобразовывать закодированное в URL тело запроса в отдельные параметры запроса. Однако если тело запроса содержит в себе XML-документ, как это происходит в случаях POST и PUT, то для преобразования XML-данных в параметры запроса, подходящие для использования в методе действия, требуется выполнить дополнительные действия.

В листинге 10 приведен пример такого XML-документа, представляющего собой запись о новой книге:

Листинг 10. Запись о новой книге в формате XML-документа
<book>
  <title>The Da Vinci Code</title>
  <author>Dan Brown</author>
</book>

Простейшим способом такой дополнительной обработки является создание подкласса класса AgaviWebRequest с целью проверки заголовка Content-Type входящего запроса. Если заголовок указывает на тело XML-запроса, выполняется необходимая обработка XML-кода. В листинге 11 приведен пример такого подкласса:

Листинг 11. Пользовательский класс обработчика HTTP-запроса
<?php
// credit: David Zuelke
class ExampleAppWebRequest extends AgaviWebRequest {
  public function initialize(AgaviContext $context, array $parameters = array()) {
    parent::initialize($context, $parameters);
    $rd = $this->getRequestData();
    if (stristr($rd->getHeader('Content-Type'), 'text/xml')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $xml = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $xml = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $xml = '';
        }
      }
      $rd->setParameters((array)simplexml_load_string($xml));
    }
  } 
}
?>

Если данные PUT и POST представлены не в URL-кодировке, они сохраняются в переменных запроса put_file и post_file. В листинге 11 данные извлекаются из запроса и с помощью SimpleXML конвертируются в объект, после чего этот объект преобразуется в массив, с которым может работать метод setParameters() класса AgaviRequestDataHolder. Теперь к этим данным можно обращаться обычным образом (с помощью методов действия) через метод getParameter() класса AgaviRequestDataHolder.

Вы можете сохранить измененное определение класса в файле $ROOT/app/lib/request/ExampleAppWebRequest.class.php, а затем загрузить его, добавив информацию в файл $ROOT/app/config/autoload.xml, как показано в листинге 12:

Листинг 12. Конфигурация автозагрузчика Agavi
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns="http://agavi.org/agavi/config/parts/autoload/1.0" 
 xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 parent="%core.system_config_dir%/autoload.xml">
  <ae:configuration>
    <autoload name="ExampleAppBaseAction">
     %core.lib_dir%/action/ExampleAppBaseAction.class.php</autoload>
    <autoload name="ExampleAppBaseModel">
    %core.lib_dir%/model/ExampleAppBaseModel.class.php</autoload>
    <autoload name="ExampleAppBaseView">
    %core.lib_dir%/view/ExampleAppBaseView.class.php</autoload>
    <autoload name="ExampleAppWebRequest">
    %core.lib_dir%/request/ExampleAppWebRequest.class.php</autoload>
    <autoload name="Doctrine">
    %core.app_dir%/../libs/doctrine/Doctrine.php</autoload>    
  </ae:configuration>
</ae:configurations>

Это еще не все. Обычное REST-соглашение подразумевает, что запрос POST создает новый ресурс, в то время как запрос PUT обновляет существующий. Однако Agavi в конфигурации по умолчанию преобразует запросы POST в метод executeWrite(), а запросы PUT – в метод executeCreate(). Это может сбить с толку REST-разработчиков, и поэтому будет разумно поменять местами эти преобразования, отредактировав файл $ROOT/app/config/factories.xml, как показано в листинге 13:

Листинг 13. Переопределение отображения предустановленных HTTP-методов запроса в методы Agavi
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/factories/1.0">
  <ae:configuration>  
    ...

    <request class="ExampleAppWebRequest">
      <ae:parameter name="method_names">
          <ae:parameter name="POST">create</ae:parameter>
          <ae:parameter name="GET">read</ae:parameter>
          <ae:parameter name="PUT">write</ae:parameter>
          <ae:parameter name="DELETE">remove</ae:parameter>
      </ae:parameter>
    </request>
  ... 
  </ae:configuration>

После выполнения этих действий можно определить метод Books_IndexAction::executeCreate() для обработки запросов POST. Код обработчика приведен в листинге 14.

Листинг 14. Обработчик IndexAction для запросов POST
<?php
class Books_IndexAction extends ExampleAppBooksBaseAction
{
  public function executeCreate(AgaviRequestDataHolder $rd)
  {
    $book = new Book;
    $book->author = $rd->getParameter('author');
    $book->title = $rd->getParameter('title');
    $book->save();    
    $this->setAttribute('result', array($book->toArray()));
    return 'Success';
  }
}
?>

Не забудьте также обновить валидатор действия, чтобы обеспечить использование этих переменных запроса (листинг 15):

Листинг 15. Валидатор IndexAction
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Books/config/validators.xml"
>
  <ae:configuration>

    <validators method="create">
      <validator class="string">
        <arguments>
          <argument>author</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
      <validator class="string">
        <arguments>
          <argument>title</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>

  </ae:configuration>
</ae:configurations>

REST-соглашения требуют, чтобы в ответе на успешный запрос POST содержались код состояния 201 (Created), заголовок размещения с URL-адресом нового ресурса, а также содержимое ресурса в теле ответа. Все это достигается путем небольшого изменения представления IndexSuccessView, как показано в листинге 16.

Листинг 16. Исправленное XML-представление IndexSuccess
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    if ($this->getContext()->getRequest()->getMethod() == 'create') {
        $this->getResponse()->setHttpStatusCode('201');
        $this->getResponse()->setHttpHeader(
         'Location', 
         $this->getContext()->getRouting()->gen(
          'book', 
          array('id' => $result[0]['id']
        )));     
    }
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);      
    }
    return $xml->asXml();
  }
}
?>

Обработка запросов PUT и DELETE

Как было замечено ранее, запросы PUT используются для отображения изменения существующего ресурса и по существу содержат в строке запроса идентификатор ресурса. Успешный запрос PUT подразумевает, что существующий ресурс был заменен ресурсом, указанным в теле запроса PUT. Ответом на успешный запрос PUT может являться либо код состояния 200 (OK) с телом ответа, содержащим обновленный ресурс, либо код состояния 204 (No Content) с пустым телом ответа.

В листинге 17 содержится исправленное определение валидатора:

Листинг 17. Валидатор BookAction
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Books/config/validators.xml"
>
  <ae:configuration>
    
    <validators>
      <validator class="number">
        <arguments>
          <argument>id</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>

    <validators method="write">
      <validator class="string">
        <arguments>
          <argument>author</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
      <validator class="string">
        <arguments>
          <argument>title</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>

  </ae:configuration>
</ae:configurations>

В листинге 18 содержится код метода Books_BookAction::executeWrite(), который будет обрабатывать запросы PUT:

Листинг 18. Обработчик BookAction для запросов PUT
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    $book = Doctrine::getTable('book')->find($rd->getParameter('id'));
    if (!is_object($book)) {
      return 'Error';
    }
    $book->author = $rd->getParameter('author');
    $book->title = $rd->getParameter('title');
    $book->save();
    $this->setAttribute('result', array($book->toArray()));
    return 'Success';
  }
}
?>

Аналогичным образом метод executeRemove() будет обрабатывать запросы DELETE, удаляя указанный ресурс из хранилища данных. Код этого метода содержится в листинге 19:

Листинг 19. Обработчик BookAction для запросов DELETE
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeRemove(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->delete('Book')
          ->addWhere('id = ?', $rd->getParameter('id'));
    $result = $q->execute();
    $this->setAttribute('result', null);
    return 'Success';
  }
}
?>

Ответом на успешный запрос DELETE может быть либо помещенный в тело ответа код состояния 200 (OK), либо код состояния 204 (No Content) и пустое тело ответа. Последнее легко достигается путем изменения представления BookSuccessView, как показано в листинге 20:

Листинг 20. XML-представление BookSuccess
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    if ($this->getContext()->getRequest()->getMethod() == 'remove') {
      $this->getResponse()->setHttpStatusCode('204');
      return false;
    }
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);
    }
    return $xml->asXml();
  }
}
?>

Добавляем поддержку JSON

В предыдущих разделах было показано, как настроить простой интерфейс REST API на базе XML. Тем не менее все большую популярность набирает формат обмена данными JSON, и часто бывает необходимо обеспечить в теле ответа вашего интерфейса REST API поддержку JSON-запросов. При помощи гибких выходных типов Agavi вы можете с легкостью сделать это.

Шаг 1. Активация выходного типа JSON

Первым шагом добавьте в таблицу маршрутизации обработчик для JSON-запросов, как показано в листинге 21:

Листинг 21. Обработчик маршрута JSON
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      <!-- обработчик для JSON-запросов -->
      <route name="json" pattern=".json$" cut="true" stop="false" 
       output_type="json" />
      ...
    </routes>
  </ae:configuration>
</ae:configurations>

Этот маршрут, помещенный в начало таблицы маршрутизации, действует для всех запросов, оканчивающихся суффиксом .json, и устанавливает для них использование выходного типа JSON. Однако он делает еще кое-что:

  • Атрибут cut определяет, следует ли удалять указанную подстроку из запрашиваемого URL-адреса перед его обработкой. В нашем случае атрибута имеет значение true, так что при совпадении суффикс .json удаляется из запрашиваемого URL.
  • Атрибут stop в определении маршрута определяет, будет ли обработка маршрута продолжаться после первого совпадения. В нашем случае значение атрибута установлено в false, что гарантирует, что обработка запроса продолжится до конца списка, пока не произойдет совпадение в оставшейся части URL и не будет вызвано соответствующее действие Action.

Результирующий эффект данной конфигурации заключается в том, что когда Agavi получает запрос, например, для http://example.localhost/books/1.json, в таблице маршрутизации выполняется поиск прямого совпадения с общим маршрутом верхнего уровня. После этого Agavi убирает суффикс .json из URL и устанавливает для запроса выходной тип JSON. После этого оставшаяся часть URL, http://example.localhost/books/1, сравнивается с перечисленными маршрутами до тех пор, пока не будет найдено совпадения с маршрутом books.book, и не будет вызван обработчик BookAction. После завершения работы BookAction Agavi найдет в представлении метод executeJson(), выполнит его и возвратит результат клиенту.

Далее определим новый выходной тип JSON (листинг 22):

Листинг 22. Определение выходного типа JSON
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/output_types/1.0">
  <ae:configuration>
    <output_types default="xml">      
      <output_type name="json">
        <ae:parameter name="http_headers">
          <ae:parameter name="Content-Type">application/json</ae:parameter>
        </ae:parameter>
      </output_type>
      ...
    </output_types>    
  </ae:configuration>  
</ae:configurations>

Шаг 2. Извлечение параметров из Web-запросов JSON

Как и в случае с XML, Agavi не будет автоматически преобразовывать JSON-пакеты в параметры запроса. Таким образом, для выполнения этой задачи нам необходимо обновить пользовательский класс ExampleAppWebRequest, используя заголовок Content-Type и функцию PHP json_decode() для извлечения значений JSON в PHP-массив, который может быть передан в метод setParameters() класса AgaviRequestDataHolder. Код обработчика приведен в листинге 23:

Листинг 23. Исправленный обработчик Web-запроса
<?php
class ExampleAppWebRequest extends AgaviWebRequest {
  public function initialize(AgaviContext $context, array $parameters = array()) {
    parent::initialize($context, $parameters);
    $rd = $this->getRequestData();
    // обработка XML-запросов
    if (stristr($rd->getHeader('Content-Type'), 'text/xml')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $xml = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $xml = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $xml = '';
        }
      }
      $rd->setParameters((array)simplexml_load_string($xml));
    }
    // обработка JSON-запросов
    if (stristr($rd->getHeader('Content-Type'), 'application/json')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $json = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $json = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $json = '{}';
        }
      }
      $rd->setParameters(json_decode($json, true));
    } 
  } 
}
?>

Шаг 3. Обновление представлений приложения

На последнем шаге нужно обновить несколько представлений приложения, обеспечив поддержку выходных данных JSON. Для этого добавьте методы executeJson() в представления IndexSuccessView (листинг 24) и BookSuccessView (листинг 25):

Листинг 24. JSON-представление IndexSuccess
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{ 
  public function executeJson(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    if ($this->getContext()->getRequest()->getMethod() == 'create') {
      $this->getResponse()->setHttpStatusCode('201');
      $this->getResponse()->setHttpHeader('Location', 
       $this->getContext()->getRouting()->gen(
        'book', 
        array('id' => $result[0]['id']
      ))); 
    }
    return json_encode($result);
  }
}
?>
Листинг 25. JSON-представление BookSuccess
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeJson(AgaviRequestDataHolder $rd)
  {
    if ($this->getContext()->getRequest()->getMethod() == 'remove') {
      $this->getResponse()->setHttpStatusCode('204');     
      return false;
    }
    return json_encode($this->getAttribute('result'));
  }
}
?>

Чтобы увидеть все это в действии, введите в адресной строке Web-браузера http://example.localhost/books/.json или http://example.localhost/books/1.json. Вы должны получить ответ в JSON-кодировке, содержащий соответствующие данные. На рисунке 5 показан вид такого ответа в отладчике Firebug (посмотреть текстовую версию рисунка 5).

Рисунок 5. JSON-ответ на запрос GET для всех книг

Важно обратить внимание на то, что мы добились вышеописанной поддержки JSON путем простых изменений в нескольких представлениях, в то время как код действий остался неизменным. Оставляя на усмотрение разработчиков решение о способах обработки различных выходных типов в представлениях, а не в действиях, Agavi сводит к минимуму дублирование кода, в тоже время реализуя принципы MVC и DRY (Don't Repeat Yourself – не повторяй себя).


Заключение

Agavi является полнофункциональной инфраструктурой для разработки интерфейсов REST API, позволяющим авторам приложений предоставлять сторонним разработчикам доступ к функциям своих приложений с помощью легковесного и наглядного архитектурного шаблона. Встроенная поддержка REST-маршрутов, а также способность Agavi легко обеспечивать поддержку новых выходных типов делают эту платформу идеальной для быстрой разработки и развертывания API-интерфейсов. Реализация модели MVC в Agavi также означает, что вы можете добавить интерфейс REST API в приложение на любой стадии его разработки или даже после его развертывания при минимальном вмешательстве в существующую бизнес-логику.

В разделе Загрузка вы найдете весь представленный в этой статье код, а также простой тестовый сценарий на основе jQuery, с помощью которого вы можете выполнять запросы GET, POST, PUT и DELETE для тестового API-интерфейса. Я советую вам загрузить этот код, поупражняться с ним и, может быть, попробовать добавить к нему несколько новых функций. Я гарантирую, что вы ничего не потеряете и определенно получите новые знания. Удачи вам!


Загрузка

ОписаниеИмяРазмер
Примеры к статьеexample-app-rest.zip3775 КБ

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML, Open source
ArticleID=645817
ArticleTitle=Создание интерфейса REST API в Agavi
publish-date=04082011