Содержание


Реализация SOAP-сервисов с помощью Zend Framework

Как с помощью Zend Framework быстро добавить SOAP-сервисы к Web-приложению, написанному на PHP

Comments

Сегодня Web-сервисам уделяется много внимания, причем львиная его доля приходится на сервисы на основе REST. REST стал популярен благодаря своей простой, интуитивно понятной природе и способности работать с существующими методами HTTP. Но стоит помнить, что REST ― не самый лучший вариант: SOAP (Simple Object Access Protocol) обеспечивает более формальный, стандартизированный подход к решению проблемы обмена информацией через Интернет.

Хотя SOAP-сервисы обычно считают сложными и трудоемкими в реализации, существуют инструменты, которые значительно упрощают процесс. Один из них ― Zend Framework, который представляет собой полную MVC-среду для создания масштабируемых Web-приложений на языке PHP. Наряду с массой других полезных вещей – ООР-формами, поддержкой i18n, кэшированием запросов и страниц, интеграцией Dojo и т.п. - Zend Framework обеспечивает полный набор инструментов для создания и развертывания SOAP-сервисов с помощью компонента Zend_Soap.

В этой статье мы рассмотрим процесс создания простого Web-сервиса на основе SOAP с помощью Zend Framework. Мы не только покажем, как обрабатывать клиентские запросы и возвращать SOAP-совместимые ответы, но и исследуем процесс обработки исключений и создания сообщений об ошибках SOAP. Наконец, мы используем Zend_Soap для автоматического создания файла WSDL, описывающего сервис SOAP, чтобы наделить клиенты способностью "автообнаружения" API SOAP-сервиса.

Что такое SOAP

Сначала несколько слов о SOAP. Это способ обмена информацией через Интернет с помощью не зависящего от языка кода XML, что позволяет приложениям, написанным на разных языках, устанавливать связь друг с другом. Этот код XML передается между клиентом и сервером с помощью HTTP в качестве транспортного протокола с использованием сильной типизации данных для обеспечения их целостности.

В отличие от REST, который связан с ресурсами и действиями, SOAP основан на методах и типах данных. Если REST-сервис, как правило, ограничивается четырьмя операциями, соответствующими четырем HTTP-методам: GET, POST, PUT и DELETE, то SOAP-сервис не имеет таких ограничений; он способен обеспечить столько методов, сколько определит разработчик. Более того, эти методы, как правило, вызываются с помощью HTTP-метода POST, причем этот метод никак не привязан к типу вызываемой операции.

Чтобы проиллюстрировать, как работает SOAP, рассмотрим простой пример. Предположим, что у вас есть приложение для создания закладок в социальной сети, и вы хотите разрешить сторонним разработчикам добавлять и удалять закладки из этого приложения с помощью SOAP. Обычно создают набор сервис-объектов с такими методами, как getBookmark() и addBookmark(), и публикуют эти сервис-объект на сервере SOAP. Сервер осуществляет перевод типов данных SOAP в стандартные типы данных, анализирует пакеты запросов SOAP, реализует соответствующий метод сервера, а также генерирует пакеты ответов SOAP с результатами.

В листинге 1 приведен пример SOAP-запроса для процедуры getBookmark().

Листинг 1. Пример SOAP-запроса
POST /soap HTTP/1.1
Host: localhost
Connection: Keep-Alive
User-Agent: PHP-SOAP/5.3.1
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 471

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getBookmark env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<param0 xsi:type="xsd:int">4682</param0>
</ns1:getBookmark>
</env:Body>
</env:Envelope>

А в листинге 2 иллюстрируется пример ответа.

Листинг 2. Пример SOAP-ответа
HTTP/1.1 200 OK
Date: Wed, 17 Mar 2010 17:13:28 GMT
Server: Apache/2.2.14 (Win32) PHP/5.3.1
X-Powered-By: PHP/5.3.1
Content-Length: 800
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/soap+xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc">
<ns1:getBookmarkResponse 
 env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<rpc:result>return</rpc:result>
<return enc:itemType="xsd:string" enc:arraySize="3" 
 xsi:type="enc:Array">
<item xsi:type="xsd:string">http://www.google.com</item>
<item xsi:type="xsd:string">http://www.php-programming-solutions.com
</item>
<item xsi:type="xsd:string">http://www.mysql-tcr.com</item>
</return>
</ns1:getBookmarkResponse>
</env:Body>
</env:Envelope>

В типичной операции SOAP сервер принимает запрос в коде XML, как в листинге 1, анализирует XML, выполняет соответствующий метод сервис-объекта и возвращает клиенту ответ в коде XML, как в листинге 2. Клиент, как правило, в состоянии разобрать и перевести этот SOAP-ответ в объект конкретного языка или структуры данных для дальнейшей обработки. Можно использовать необязательный файл WSDL, чтобы предоставить клиентам информацию о доступных методах, а также количество и типы данных входных параметров и возвращаемых значений.

В Zend Framework входят реализации для клиента и сервера SOAP, а также для автоматической генерации файлов WSDL. Реализации для сервера и клиента - это оболочки вокруг SOAP-расширения в PHP; это означает, что они не будут работать, если сборка PHP не включает в себя поддержку SOAP-расширения. Тем не менее, использование библиотеки Zend Framework поверх стандартного расширения немного упрощает задачу, так как разработчику достаточно определить набор объектов, который реализует API сервиса, и прикрепить их к серверу для обработки входящих запросов. В следующих разделах мы обсудим это подробнее.

Установка примера приложения

Прежде чем приступить к реализации SOAP-сервиса, сделаем несколько замечаний и предположений. В этой статье я буду считать, что у вас есть действующая среда разработки с Apache, PHP+SOAP и MySQL, что Zend Framework установлен в каталоге PHP и что вы знакомы с основами SQL, XML и SOAP. Я также предполагаю, что вы знакомы с основными принципами разработки приложений с использованием Zend Framework, понимаете связь между действиями и контроллерами и знакомы с уровнем абстракции базы данных Zend_Db. Наконец, я буду считать, что ваш Web-сервер Apache настроен для поддержки виртуального хостинга и перезаписи URL посредством файлов .htaccess. Если эти предметы вам не знакомы, обратитесь по ссылкам на дополнительную информацию, приведенным в разделе Ресурсы.

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

  • getProducts(): возвращает все продукты из базы данных;
  • getProduct($id): возвращает определенный продукт из базы данных;
  • addProduct($data): добавляет новый продукт в базу данных;
  • deleteProduct($id): удаляет определенный продукт из базы данных;
  • updateProduct($id, $data): присваивает определенному продукту в базе данных новые значения.

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

Для начала установим стандартное приложение Zend Framework, которое обеспечивает контекст для кода, приведенного в этой статье. Чтобы инициализировать новый проект, используем сценарий инструмента Zend Framework (zf.bat в Windows® или zf.sh в UNIX)™, как показано ниже:

shell> zf.bat create project example

Теперь можно определить новый виртуальный хост для этого приложения, такой как http://example.localhost/ в конфигурации Apache, и указать каталог public/ приложения в качестве корневого каталога документов виртуального хоста. Если теперь перейти в этот хост, вы увидите страницу приветствия Zend Framework по умолчанию, как показано на рисунке 1.

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

Шаг 2: инициализация базы данных и модели приложения

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

mysql> CREATE TABLE IF NOT EXISTS products (
    ->   id int(11) NOT NULL AUTO_INCREMENT, 
    ->   title varchar(200) NOT NULL,
    ->   shortdesc text NOT NULL,
    ->   price float NOT NULL,
    ->   quantity int(11) NOT NULL,
    ->   PRIMARY KEY (id)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

Заполним эту таблицу примерами записей, как показано ниже.

mysql> INSERT INTO products (id, title, shortdesc, price, quantity) VALUES(1, 
    ->  'Ride Along Fire Engine', 'This red fire engine is ideal for toddlers who want 
    ->  to travel independently. Comes with flashing lights and beeping horn.', 
    ->  69.99, 11);
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO products (id, title, shortdesc, price, quantity) VALUES(2, 
    -> 'Wind-Up Crocodile Bath Toy', 'This wind-up toy is the perfect companion for hours 
    -> of bathtub fun.', 7.99, 67);
Query OK, 1 row affected (0.08 sec)

Шаг 3: настройка пространства имен приложения

Последний шаг заключается в настройке пространства имен приложения для автозагрузчика Zend Framework. Этот шаг позволяет автоматически по мере необходимости загружать в приложение связанные с ним классы. В данном случае я предполагаю, что пространством имен приложения является Example, а специализированные классы (такие как классы SOAP-сервиса) хранятся в $PROJECT/library/Example/. Изменим соответствующим образом файл конфигурации приложения $PROJECT/application/configs/application.ini и добавим к нему следующую строку:

autoloaderNamespaces[] = "Example_"

Теперь все готово, чтобы приступить к созданию SOAP-сервиса.

Извлечение данных

Поскольку это пример, я для простоты создам действие для обработки SOAP-запросов внутри IndexController модуля по умолчанию, однако в реальном приложении лучше завести отдельный контроллер для обработки SOAP-запросов. Отредактируем файл $PROJECT/application/controllers/IndexController.php и добавим к нему новое действие, как в листинге 3.

Листинг 3. Определение soapAction()
<?php
class IndexController extends Zend_Controller_Action
{
    public function soapAction()
    {
      // отключение макетов и визуализации
      $this->getHelper('viewRenderer')->setNoRender(true);

      // инициализация сервера и задание URI
      $server = new Zend_Soap_Server(null, 
        array('uri' => 'http://example.localhost/index/soap'));

      // задание класса SOAP-сервиса
      $server->setClass('Example_Manager');

      // обработка запроса
      $server->handle();
    }
}

Листинг 3 инициализирует новый объект Zend_Soap_Server в не-WSDL режиме, передавая конструктору объекта в качестве первого аргумента нулевое значение. При настройке сервера в не-WSDL режиме необходимо указать URI сервера; в листинге 3 он указан в массиве опций, передаваемом конструктору в качестве второго аргумента.

Далее, метод объекта сервера setClass() используется для подключения к серверу класса сервиса. Этот класс реализует методы SOAP-сервиса; сервер будет автоматически вызывать их в ответ на запросы SOAP. К серверу можно добавить специальные функции с помощью методов addFunction() и loadFunctions(), вместо присоединения класса методом setClass().

Как отмечалось выше, класс Zend_Soap_Server не обеспечивает реализацию SOAP-сервера; он всего лишь создает оболочку вокруг встроенного в PHP расширения SOAP. Так что когда все приготовления останутся позади, метод handle() в листинге 3 позаботится об инициализации встроенного объекта PHP SoapServer, передавая ему объект запроса и вызывая метод handle() этого объекта для обработки SOAP-запроса.

Все это хорошо, но мы пока не очень далеко продвинулись, так как еще не определен класс сервиса. Создадим его, используя код листинга 4 и сохранив результирующее определение класса в $PROJECT/library/Example/Manager.php.

Листинг 4. Объект сервиса с определенными методами get*()
<?php
class Example_Manager {

    /**
     * Возвращает список всех продуктов в базе данных
     *
     * @return array
     */
    public function getProducts() 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $sql = "SELECT * FROM products";      
      return $db->fetchAll($sql);      
    }

    /**
     * Возвращает указанный продукт в базе данных
     *
     * @param integer $id
     * @return array|Exception
     */
    public function getProduct($id) 
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');          
      }
      $db = Zend_Registry::get('Zend_Db');        
      $sql = "SELECT * FROM products WHERE id = '$id'";   
      $result = $db->fetchAll($sql);      
      if (count($result) != 1) {        
        throw new Exception('Invalid product ID: ' . $id);  
      } 
      return $result;  
    }
}
?>

Листинг 4 устанавливает автономный класс сервиса, содержащий два метода. Метод getProducts() использует Zend_Db, чтобы получить все доступные записи о продуктах из таблицы и вернуть их в виде массива, в то время как метод getProduct() принимает идентификатор продукта и возвращает только указанную запись. Затем SOAP-сервер преобразует значения, возвращенные методом, в пакет ответа SOAP и возвратит его запрашивающему клиенту. В листинге 8 приведен пример такого ответного пакета.

Если вам интересно, где инициализирован Zend_Db, то это делается в загрузчике приложения, в $PROJECT/application/Bootstrap.php. Файл Bootstrap.php содержит функцию _initDatabase(), которая устанавливает адаптер Zend_Db и регистрирует его в реестре приложения. Код приведен в листинге 5.

Листинг 5. Инициализация адаптера базы данных
<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
  protected function _initDatabase()
  {
    $db = new Zend_Db_Adapter_Pdo_Mysql(array(
        'host'     => 'localhost',
        'username' => 'user',
        'password' => 'pass',
        'dbname'   => 'example'
    ));
    Zend_Registry::set('Zend_Db', $db); 
  }
}

Чтобы увидеть это в действии, создалим SOAP-клиент (листинг 6) и используем его для подключения к SOAP-службе и вызова метода getProducts().

Листинг 6. Пример SOAP-клиента
<?php
// загрузка библиотек Zend 
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Soap_Client');

// инициализация SOAP-клиента
$options = array(
  'location' => 'http://example.localhost/index/soap',
  'uri'      => 'http://example.localhost/index/soap'
);

try {
  $client = new Zend_Soap_Client(null, $options);  
  $result = $client->getProducts();
  print_r($result);
} catch (SoapFault $s) {
  die('ERROR: [' . $s->faultcode . '] ' . $s->faultstring);
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

SOAP-клиент сгенерирует пакет запроса (листинг 7).

Листинг 7. Пример запроса метода SOAP getProducts()
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getProducts env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"/>
</env:Body>
</env:Envelope>

Сервер ответит на это в коде SOAP (листинг 8).

Листинг 8. Пример ответа SOAP на метод getProducts()
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:ns2="http://xml.apache.org/xml-soap" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc">
<ns1:getProductsResponse 
 env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<rpc:result>return</rpc:result>
<return enc:itemType="ns2:Map" enc:arraySize="2" xsi:type="enc:Array">
<item xsi:type="ns2:Map">
<item>
<key xsi:type="xsd:string">id</key>
<value xsi:type="xsd:string">1</value>
</item>
<item>
<key xsi:type="xsd:string">title</key>
<value xsi:type="xsd:string">Ride Along Fire Engine</value>
</item>
<item>
<key xsi:type="xsd:string">shortdesc</key>
<value xsi:type="xsd:string">This red fire engine is ideal 
 for toddlers who want to travel independently. 
 Comes with flashing lights and beeping horn.</value>
</item>
<item>
<key xsi:type="xsd:string">price</key>
<value xsi:type="xsd:string">69.99</value>
</item>
<item>
<key xsi:type="xsd:string">quantity</key>
<value xsi:type="xsd:string">11</value>
</item>
</item>
...
</return>
</ns1:getProductsResponse>
</env:Body>
</env:Envelope>

Затем SOAP-клиент преобразует этот ответ обратно в стандартный массив PHP, который можно обрабатывать или исследовать дальше, как показано на рисунке 2.

Рисунок 2. Результат запроса SOAP, преобразованный в стандартный массив PHP
Результат запроса SOAP, преобразованный в стандартный массив PHP
Результат запроса SOAP, преобразованный в стандартный массив PHP

Добавление, удаление и изменение данных

Все это относится к получению данных посредством SOAP. А как насчет добавления и удаления данных?

В классе Example_Manager достаточно легко реализовать метод addProduct(). Это демонстрируется в листинге 9.

Листинг 9. Объект SOAP-сервиса с определенным методом addProduct()
<?php
class Example_Manager 
{
   /**
     * добавляет новый продукт в базу данных
     *
     * @param array $data array of data values with keys -> table fields
     * @return integer id of inserted product
     */
    public function addProduct($data) 
    {      
      $db = Zend_Registry::get('Zend_Db');        
      $db->insert('products', $data);
      return $db->lastInsertId();
    }
}

Метод addProduct() в листинге 9 принимает новую запись о продукте как массив пар ключ-значение, а затем использует метод insert() объекта Zend_Db для занесения этой записи в таблицу базы данных. Он возвращает идентификатор вставленной записи.

Удаление продуктов осуществляется столь же просто: достаточно добавить метод deleteProduct(), который принимает идентификатор продукта в качестве входных данных, а затем использует метод Zend_Db delete() для удаления записи из базы данных. Этот метод иллюстрируется в листинге 10.

Листинг 10. Объект SOAP-сервиса с определенным методом deleteProduct()
<?php
class Example_Manager 
{
    /**
     * удаляет определенный продукт из базы данных
     *
     * @param integer $id
     * @return integer number of products deleted
     */
    public function deleteProduct($id) 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $count = $db->delete('products', 'id=' . $db->quote($id));
      return $count;
    }
}

В листинге 10 второй аргумент, передаваемый методу delete(), указывает ограничение или фильтр, который будет использоваться при выполнении операции удаления. Этот аргумент важен, так как без него Zend_Db удалит из таблицы все записи.

Наконец, листинг 11 иллюстрирует метод updateProduct(), который можно использовать для изменения записи о продукте. Этот метод принимает два входных аргумента - идентификатор продукта и массив, содержащий измененную запись, и использует метод Zend_Db update() для выполнения запроса UPDATE к таблице базы данных.

Листинг 11. Объект SOAP-сервиса с определенным методом updateProduct()
<?php
class Example_Manager 
{
    /**
     * Изменяет продукт в базе данных
     *
     * @param integer $id
     * @param array $data
     * @return integer number of products updated
     */
    public function updateProduct($id, $data) 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $count = $db->update('products', $data, 'id=' . $db->quote($id));
      return $count;        
    }
}

Можно попробовать проделать все это с SOAP-клиентом, как показано в листинге 12.

Листинг 12. Пример SOAP-клиента
<?php
// загрузка библиотек Zend 
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Soap_Client');

// инициализация SOAP-клиента
$options = array(
  'location' => 'http://example.localhost/index/soap',
  'uri'      => 'http://example.localhost/index/soap'
);

try {
  // добавление нового продукта
  // получение и отображение идентификатора продукта
  $p = array(
    'title'     => 'Spinning Top',
    'shortdesc' => 'Hours of fun await with this colorful spinning top. 
      Includes flashing colored lights.',
    'price'     => '3.99',
    'quantity'  => 57 
  );
  $client = new Zend_Soap_Client(null, $options);  
  $id = $client->addProduct($p);
  echo 'Added product with ID: ' . $result;

  // изменение существующего продукта
  $p = array(
    'title'     => 'Box-With-Me Croc',
    'shortdesc' => 'Have fun boxing with this inflatable crocodile, 
      made of tough, washable rubber.',
    'price'     => '12.99',
    'quantity'  => 25 
  );
  $client->updateProduct($id, $p);
  echo 'Updated product with ID: ' . $id;

  // удаление существующего продукта
  $client->deleteProduct($id);
  echo 'Deleted product with ID: ' . $id;  
} catch (SoapFault $s) {
  die('ERROR: [' . $s->faultcode . '] ' . $s->faultstring);
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

Сообщения об ошибках SOAP

Одна из проблем всех методов, рассмотренных в предыдущих разделах: они не включают никакой проверки или фильтров входных данных. В реальном приложении отсутствие такой проверки будет иметь серьезные последствия для целостности баз данных и может быстро привести к искажеию данных (в лучшем случае) или прямому вандализму (в худшем).

К счастью, Zend Framework содержит компонент Zend_Validate, который предоставляет встроенные средства проверки данных для наиболее распространенных сценариев. Эту функцию можно объединить с методом Zend_Soap_Server registerFaultException() для проверки данных в запросе клиента и отправки сообщения об ошибке SOAP при различных сценариях.

Чтобы увидеть, как это работает, начнем с создания специального класса исключений, расширяющего Zend_Exception, как показано в листинге 13.

Листинг 13. Специальный класс исключений
<?php
class Example_Exception extends Zend_Exception {}

Сохраним этот класс в $PROJECT/library/Example/Exception.php.

Затем добавим в различные методы класса сервиса проверку входных данных и выдачу специального сообщения об ошибке, если входные данные некорректны или отсутствуют. Листинг 14 иллюстрирует отредактированный класс Example_Manager.

Листинг 14. Пересмотренный объект SOAP-сервиса с проверкой входных данных и выдачей сообщений
<?php
class Example_Manager {

    // определение фильтров и валидаторов входных данных
    private $_filters = array(
      'title'     => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'shortdesc' => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'price'     => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'quantity'  => array('HtmlEntities', 'StripTags', 'StringTrim')
    );

    private $_validators = array(
      'title'     => array(),
      'shortdesc' => array(),
      'price'     => array('Float'),
      'quantity'  => array('Int')
    );

    /**
     * Возвращает список всех продуктов в базе данных
     *
     * @return array
     */
    public function getProducts() 
    {
      $db = Zend_Registry::get('Zend_Db');
      $sql = "SELECT * FROM products";
      return $db->fetchAll($sql);
    }

    /**
     * Возвращает указанный продукт из базы данных
     *
     * @param integer $id
     * @return array|Example_Exception
     */
    public function getProduct($id)
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');
      }
      $db = Zend_Registry::get('Zend_Db');
      $sql = "SELECT * FROM products WHERE id = '$id'";
      $result = $db->fetchAll($sql);
      if (count($result) != 1) {
        throw new Example_Exception('Invalid product ID: ' . $id); 
      } 
      return $result;
    }

    /**
     * добавляет новый продукт в базу данных
     *
     * @param array $data array of data values with keys -> table fields
     * @return integer id of inserted product
     */
    public function addProduct($data) 
    {
      $input = new Zend_Filter_Input($this->_filters,
        $this->_validators, $data);
      if (!$input->isValid()) {
        throw new Example_Exception('Invalid input');
      }
      $values = $input->getEscaped();
      $db = Zend_Registry::get('Zend_Db');
      $db->insert('products', $values);
      return $db->lastInsertId();
    }

    /**
     * удаляет определенный продукт из базы данных
     *
     * @param integer $id
     * @return integer number of products deleted
     */
    public function deleteProduct($id) 
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');
      }
      $db = Zend_Registry::get('Zend_Db');
      $count = $db->delete('products', 'id=' . $db->quote($id));
      return $count;
    }

    /**
     * Изменяет продукт в базе данных
     *
     * @param integer $id
     * @param array $data
     * @return integer number of products updated
     */
    public function updateProduct($id, $data) 
    {
      $input = new Zend_Filter_Input($this->_filters, 
        $this->_validators, $data);
      if (!Zend_Validate::is($id, 'Int') || !$input->isValid()) {
        throw new Example_Exception('Invalid input');
      } 
      $values = $input->getEscaped();
      $db = Zend_Registry::get('Zend_Db');
      $count = $db->update('products', $values, 'id=' . $db->quote($id));
      return $count;
    }    

}

В листинге 14 API сервиса дополнен проверкой всех входных параметров. Для большинства методов API статический метод Zend_Validate::is() обеспечивает удобный способ проверки входных аргументов, а в некоторых случаях для проверки и фильтрации ввода используется дополнительная цепочка фильтров Zend_Filter_Input. Любые ошибки, возникающие в процессе проверки ввода, представляются как экземпляры класса Example_Exception.

Последний шаг ― заставить сервер SOAP автоматически преобразовывать вызванные Example_Exceptions в сообщения об ошибках SOAP. Это делается путем регистрации класса исключений в сервере SOAP с использованием метода registerFaultException(), как в отредактированном IndexController::soapAction в листинге 15.

Листинг 15. Отредактированное определение SOAPAction() с поддержкой вызова специальных исключений в качестве сообщений об ошибках
<?php
class IndexController extends Zend_Controller_Action
{

    public function soapAction()
    {
      // отключение макетов и визуализации
      $this->getHelper('viewRenderer')->setNoRender(true);

      // инициализация сервера и задание URI
      $server = new Zend_Soap_Server(null, 
        array('uri' => 'http://example.localhost/index/soap'));

      // задание класса службы SOAP      
      $server->setClass('Example_Manager');

      // регистрирация исключений, которые генерируют сообщения об ошибках SOAP
      $server->registerFaultException(array('Example_Exception'));

      // обработка запроса
      $server->handle();
    }
}

Чтобы увидеть это в действии, попробуйте отправить запрос SOAP для метода getProduct() и передать ему недействительный ID. Листинг 16 содержит пример такого запроса SOAP.

Листинг 16. SOAP-запрос с недействительными входными аргументами
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getProduct env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<param0 xsi:type="xsd:string">nosuchproduct</param0>
</ns1:getProduct>
</env:Body>
</env:Envelope>

Сервер проверит входные данные и, найдя их недействительными, вызовет исключение Example_Exception, которое будет преобразовано в сообщение об ошибке SOAP, возвращаемое клиенту. Листинг 17 иллюстрирует ответный пакет.

Листинг 17. Создание сообщения об ошибке SOAP
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope 
 xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/>
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>Receiver</faultcode>
<faultstring>Invalid input</faultstring>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

С точки зрения клиента SOAP хорошая идея ― обернуть вызов SOAP в блок Try-Catch, чтобы ошибки SOAP вроде той, что описана выше, можно было легко перехватывать и обрабатывать. Если вы вернетесь к примеру клиента SOAP из листинга 12, то увидите, как это можно сделать.

Добавление поддержки WSDL

Один из недостатков стандартного расширения SOAP в PHP ― это то, что оно не включает поддержку автоматической генерации WSDL-файлов для SOAP-сервиса. WSDL-файлы полезны тем, что содержат информацию о доступных методах API SOAP и могут использоваться путем подключения клиентов к "автоматическому обнаружению" API SOAP.

Однако Zend Framework содержит компонент Zend_Soap_AutoDiscover, который можно использовать в целях автоматического создания WSDL-файла для SOAP-сервиса. Он делает это путем чтения комментариев PHPDoc в классе SOAP-сервиса и преобразования этих комментариев в документ WSDL. Если вы просмотрите предыдущие листинги из этой статьи, то увидите, что каждый метод сопровождается комментарием PHPDoc; это сделано преднамеренно, чтобы упростить автоматическое создание WSDL.

Листинг 18 показывает, как настроить автоматическое создание WSDL с помощью компонента Zend_Soap_AutoDiscover.

Листинг 18. Определение wsdlAction()
<?php
class IndexController extends Zend_Controller_Action
{
    public function soapAction()
    {
      // отключение макетов и визуализации
      $this->getHelper('viewRenderer')->setNoRender(true);

      // инициализация сервера и задание местоположения файла WSDL
      $server = new Zend_Soap_Server('http://example.localhost/index/wsdl');
      // задание класса службы SOAP      
      $server->setClass('Example_Manager');

      // регистрирация исключений, которые генерируют сообщения об ошибках SOAP
      $server->registerFaultException(array('Example_Exception'));

      // обработка запроса
      $server->handle();
    }

    public function wsdlAction()
    {
      // отключение макетов и визуализации
      $this->getHelper('viewRenderer')->setNoRender(true);

      // настройка автоматического обнаружения WSDL
      $wsdl = new Zend_Soap_AutoDiscover();

      // задание класса SOAP-сервиса
      $wsdl->setClass('Example_Manager');

      // настройка действия SOAP URI 
      $wsdl->setUri('http://example.localhost/index/soap');

      // обработка запроса
      $wsdl->handle();
    }
}

В листинге 18 определен новый метод wsdlAction(), который инициализирует экземпляр компонента Zend_Soap_AutoDiscover и указывает ему на класс Example_Manager. Вызов метода handle() этого экземпляра приводит к чтению указанного класса, анализу содержащихся в нем комментариев PHPDoc и созданию соответствующего стандартизованного документа WSDL, который полностью описывает объект сервиса.

Чтобы увидеть результат, откройте в браузере ссылку http://example.localhost/index/wsdl, и вы должны увидеть нечто похожее на рисунок 3.

Рисунок 3. Динамически генерируемый файл WSDL
Динамически генерируемый файл WSDL
Динамически генерируемый файл WSDL

Теперь вместо того, чтобы вручную указывать параметры uri и location, можно ожидать, что сервер и клиент SOAP будут использовать этот файл WSDL. Это демонстрирует и листинг 18, в котором soapAction() изменен таким образом, чтобы передавать URL WSDL в конструктор Zend_Soap_Server, который будет запускаться в режиме WSDL. Подсоединенные клиенты SOAP также могут использовать этот URL WSDL для автоматического обнаружения API SOAP-сервиса.

Заключение

Zend Framework предоставляет полный набор инструментов для быстрого и эффективного добавления API SOAP в Web-приложение. С помощью этого набора инструментов можно организовать экономичный и эффективный обмен информацией между Web-приложениями с использованием хорошо известного стандарта SOAP. Встроенная в Zend Framework поддержка SOAP-клиентов и серверов, а также автоматическое создание WSDL делает его хорошим способом быстрой реализации и развертывания SOAP-сервисов. И, наконец, так как Zend Framework ― это MVC-совместимая среда, API SOAP очень легко применить к существующим приложениям Zend Framework при минимальном влиянии на код (и, соответственно, с меньшими переделками).

Ссылки на все примеры из этой статьи, а также на простой клиент SOAP, который можно использовать для тестирования процедур добавления, редактирования, удаления и извлечения продуктов, приведены в разделе Загрузки. За инструментами, используемыми в этой статье, обращайтесь к разделу Ресурсы. Я рекомендую загрузить код, поиграть с ним и, возможно, попробовать свои силы в добавлении к нему новых возможностей. Я гарантирую вам, что вы ничего не сломаете, и это, безусловно, будет способствовать вашему обучению. Удачи!


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML, Open source, SOA и web-сервисы
ArticleID=781093
ArticleTitle=Реализация SOAP-сервисов с помощью Zend Framework
publish-date=12142011