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

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

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

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

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



14.12.2011

Введение

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

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

  • API: Application programming interface – интерфейс прикладных программ
  • HTTP: Hypertext Transfer Protocol
  • i18n: интернационализация
  • MVC: Model-View-Controller
  • OOP: Object-Oriented Programming ― объектно-ориентированное программирование
  • REST: Representational State Transfer
  • SQL: Structured Query Language - язык структурированных запросов
  • URI: Uniform Resource Identifier ― универсальный идентификатор ресурса
  • URL: Uniform Resource Locator - универсальный указатель ресурса
  • W3C: World Wide Web Consortium
  • WSDL: Web Services Description Language – язык описания Web-сервисов
  • XML: Extensible Markup Language – расширяемый язык разметки

Хотя 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 по умолчанию

Шаг 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. А как насчет добавления и удаления данных?

В классе 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

Теперь вместо того, чтобы вручную указывать параметры 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, который можно использовать для тестирования процедур добавления, редактирования, удаления и извлечения продуктов, приведены в разделе Загрузки. За инструментами, используемыми в этой статье, обращайтесь к разделу Ресурсы. Я рекомендую загрузить код, поиграть с ним и, возможно, попробовать свои силы в добавлении к нему новых возможностей. Я гарантирую вам, что вы ничего не сломаете, и это, безусловно, будет способствовать вашему обучению. Удачи!


Загрузка

ОписаниеИмяРазмер
Пример приложения для данной статьиexample-app-soap.zip8 КБ

Ресурсы

Научиться

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

  • Zend Framework: загрузите последние версии Zend Framework.
  • сервер баз данных MySQL: загрузите транзакционную базу данных с открытым исходным, используемую в этой статье.
  • DB2 Express-C: бесплатная версия СУБД IBM DB2, отличная основа разработки приложений для малого и среднего бизнеса.

Комментарии

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, SOA и web-сервисы
ArticleID=781093
ArticleTitle=Реализация SOAP-сервисов с помощью Zend Framework
publish-date=12142011