Сегодня 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. Это способ обмена информацией через Интернет с помощью не зависящего от языка кода 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 по умолчанию
Шаг 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. А как насчет добавления и удаления данных?
В классе 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());
}
?>
|
Одна из проблем всех методов, рассмотренных в предыдущих разделах: они не включают никакой проверки или фильтров входных данных. В реальном приложении отсутствие такой проверки будет иметь серьезные последствия для целостности баз данных и может быстро привести к искажеию данных (в лучшем случае) или прямому вандализму (в худшем).
К счастью, 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, то увидите, как это можно сделать.
Один из недостатков стандартного расширения 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
Теперь вместо того, чтобы вручную указывать параметры 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.zip | 8 КБ | HTTP |
Научиться
- Оригинал статьи
- Спецификация W3C SOAP: расширьте свои знания в области SOAP, простого XML-протокола для обмена информацией.
- Официальный Web-сайт Zend Framework: узнайте больше об этой объектно-ориентированной среде Web-приложений с открытым исходным кодом для PHP 5 и выше.
- Знакомство с Zend Framework: переход к разработке приложений с помощью Zend Framework.
- Руководство по Zend Framework: подробнее о компоненте Zend_Soap.
- Документация API Zend Framework: познакомьтесь с документацией API для каждой версии или прочтите о последней версии в интернете.
- списки рассылки сообщества Zend Framework и вики разработчиков Zend: примите участие в сообществе Zend Framework, задавайте вопросы и получайте ответы.
Получить продукты и технологии
- Zend Framework: загрузите последние версии Zend Framework.
- сервер баз данных MySQL: загрузите транзакционную базу данных с открытым исходным, используемую в этой статье.
- DB2 Express-C: бесплатная версия СУБД IBM DB2, отличная основа разработки приложений для малого и среднего бизнеса.
Викрам Васвани (Vikram Vaswani) – основатель и президент консалтинговой фирмы Melonfire, специализирующейся на технологиях и инструментах с открытым исходным кодом. Также является автором книг Решения по программированию на PHP and Как сделать все что угодно с помощью PHP и MySQL .