Пишем SOAP клиент-серверное приложение на PHP

Источник: habrahabr
hats

Всем привет!
Так случилось, что в последнее время я стал заниматься разработкой веб-сервисов. Но сегодня топик не обо мне, а о том, как нам написать свой XML Web Service основанный на протоколе SOAP 1.2.

Я надеюсь, что после прочтения топика вы сможете самостоятельно:

  • написать свою собственную серверную реализацию веб-приложения;
  • написать свою собственную клиентскую реализацию веб-приложения;
  • написать свое собственное описание веб-сервиса (WSDL);
  • отправлять клиентом массивы однотипных данных на сервер.

Как вы могли догадаться, вся магия будет твориться с использованием PHP и встроенных классов SoapClient и SoapServer. В качестве кролика у нас будет выступать сервис по отправке sms-сообщений.

1 Постановка задачи

1.1 Границы

В начале предлагаю разобраться с тем результатом, которого мы достигнем в конце топика. Как было объявлено выше, мы будем писать сервис по отправке sms-сообщений, а если еще точнее, то к нам будут поступать сообщения из разных источников по протоколу SOAP. После чего, мы будем рассматривать в каком виде они приходят на сервер. Сам процесс постановки сообщений в очередь для их дальнейшей провайдеру, к сожалению, выходит за рамки данного поста по многим причинам.

1.2 Какими данными будем меняться?

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

  • Какой минимум данных надо посылать на сервер, чтобы отправить sms-сообщение абоненту?
  • Какой минимум данных надо посылать с сервера, чтобы удовлетворить потребности клиента?

Что-то мне подсказывает, что для этого необходимо посылать следующее:

  • номер мобильного телефона, а также
  • текст sms-сообщения.

В принципе, двух этих характеристик достаточно для отправки, но мне сразу представляется случай, как sms-ка с поздравлением о дне рождения приходит вам в 3 часа утра, или 4! В этот момент я буду всем очень благодарен за то, что про меня не забыли! Поэтому, мы также будем посылать на сервер и

  • дату отправки sms-сообщения.

Следующее, что я бы хотел отправлять на сервер, так это

  • Тип сообщения.

Данный параметр не является обязательным, но он может нам очень сильно пригодиться в случае, если быстро понадобится сказать боссу о том, скольких из наших клиентов мы "обрадовали" своим известием, а также нарисовать какую-нибудь красивую статистику на этот счет.

И все же, я что-то забыл! Если еще немного порефлексировать, то стоит отметить, что клиент за раз может отправить на сервер как одно sms-сообщение, так и некоторое их количество. Другими словами, в одном пакете данных может быть от одного до бесконечности сообщений.

В результате мы получаем, что для отправки sms-сообщения нам необходимы следующие данные:

  • номер мобильного телефона,
  • текст sms-сообщения,
  • время отправки sms-сообщения абоненту,
  • тип сообщения.

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

  • TRUE - пакет успешно дошел до сервера, прошел аутентификацию и встал в очередь для отправки sms-провайдеру
  • FALSE - во всех остальных случаях

На этом мы закончили описание постановки задачи! И наконец-то приступим к самому интересному - будем разбираться что за диковинный зверь этот SOAP!

2 С чем есть SOAP?

Вообще, изначально я не планировал ничего писать о том, что такое SOAP и хотел ограничиться ссылками на сайт w3.org с нужными спецификациями, а также ссылками на Wikipedia. Но в самом конце решил написать коротенькую справочку об этом протоколе.

И начну я свое повествование с того, что данный протокол обмена данными относится к подмножеству протоколов основанных на так называемой парадигме RPC (Remote Procedure Call, удалённый вызов процедур) антиподом которой является REST (Representational State Transfer, передача репрезентативного состояния). Более подробно об этом можно прочесть в Wikipedia, ссылки на статьи находятся в самом конце топика. Из этих статей нам надо уяснить следующее: "Подход RPC позволяет использовать небольшое количество сетевых ресурсов с большим количеством методов и сложным протоколом. При подходе REST количество методов и сложность протокола строго ограничены, из-за чего количество отдельных ресурсов может быть большим". Т.е., применительно к нам это означает, что на сайте в случае RPC подхода будет всегда один вход (ссылка) на сервис и какую процедуру вызывать для обработки поступающих данных мы передает вместе с данными, в то время как при REST подходе на нашем сайте есть много входов (ссылок), каждая из которых принимает и обрабатывает только определенные данные. Если кто-то из читающих знает, как еще проще объяснить различие в данных подходах, то обязательно пишите в комментариях!

Следующее, что нам надо узнать про SOAP - данный протокол в качестве транспорта использует тот самый XML, что с одной стороны очень хорошо, т.к. сразу же в наш арсенал попадает вся мощь стека технологий основанных на данном языке разметки, а именно XML-Schema - язык описания структуры XML-документа (спасибо Wikipedia!), который позволяет производит автоматическую валидацию поступающих на сервер данных от клиентов.

И так, теперь мы знаем, что SOAP - протокол используемый для реализации удаленного вызова процедур и в качестве транспорта он использует XML! Если почитать статью на Wikipedia, то оттуда можно узнать еще и о том, что он может использоваться поверх любого протокола прикладного уровня, а не только в паре с HTTP (к сожалению, в данном топике мы будем рассматривать только SOAP поверх HTTP). И знаете, что мне во всем этом больше всего нравится? Если нет никаких догадок, то я дам подсказку - SOAP!… Всеравно не появилось догадок?… Вы точно прочли статью на Wikipedia?… В общем, не буду вас дальше мучить. Поэтому, сразу перейду к ответу: "SOAP (от англ. Simple Object Access Protocol - простой протокол доступа к объектам; вплоть до спецификации 1.2 )". Самое примечательное в этой строчке выделено курсивом! Я не знаю какие выводы сделали вы из всего этого, но мне видится следующее - поскольку данный протокол ну никак нельзя назвать "простым" (и видимо с этим согласны даже в w3), то с версии 1.2 он вообще перестал как-то расшифровываться! И стал называться SOAP, просто SOAP и точка.

Ну да ладно, прошу меня извинить, занесло немного в сторону. Как я писал ранее, в качестве транспорта используется XML, а пакеты, которые курсируют между клиентом и сервером называются SOAP-конвертами. Если рассматривать обобщенную структуру конверта, то он вам покажется очень знакомым, т.к. напоминает разметку HTML-страницы. В нем есть основной раздел - Envelop , который включает разделы Header и Body , либо Fault . В Body передаются данные и он является обязательным разделом конверта, в то время как Header является опциональным. В Header может передаваться авторизация, либо какие-либо иные данные, которые на прямую не относятся к входным данным процедур веб-сервиса. Про Fault особо рассказывать нечего, кроме того, что он приходит в клиент с сервера в случае возникновения каких-либо ошибок.

На этом мой обзорный рассказ про протокол SOAP заканчивается (более детально сами конверты и их структуру мы рассмотрим когда наши клиент и сервер наконец-то научатся запускать их друг в друга) и начинается новый - про компаньона SOAP под названием WSDL (Web Services Description Language). Да-да, это та самая штука, которая отпугивает большинство из нас от самой попытки взять и реализовать свое API на данном протоколе. В результате чего, мы обычно изобретаем свой велосипед с JSON в качестве транспорта. И так, что такое WSDL? WSDL - язык описания веб-сервисов и доступа к ним, основанный на языке XML (с) Wikipedia. Если из этого определения вам не становится понятным весь сакральный смысл данной технологии, то я попытаюсь описать его своими словами!

WSDL предназначен для того, чтобы наши клиенты могли нормально общаться с сервером. Для этого в файле с расширением "*.wsdl" описывается следующая информация:

  • Какие пространства имен использовались,
  • Какие схемы данных использовались,
  • Какие типы сообщений веб-сервис ждет от клиентов,
  • Какие данные принадлежат каким процедурам веб-сервиса,
  • Какие процедуры содержит веб-сервис,
  • Каким образом клиент должен вызывать процедуры веб-сервиса,
  • На какой адрес должны отправляться вызовы клиента.

Как видно, данный файл и есть весь веб-сервис. Указав в клиенте адрес WSDL-файла мы будем знать об любом веб-сервисе все! В результате, нам не надо абсолютно ничего знать о том, где расположен сам веб-сервис. Достаточно знать адрес расположения его WSDL-файла! Скоро мы узнаем, что не так страшен SOAP как его малюют (с) русская пословицы.

3 Введение в XML-Schema

Теперь мы много чего знаем о то, что такое SOAP, что находится у него внутри и имеем обзорное представление о том, какой стек технологий его окружает. Поскольку, прежде всего SOAP представляет собой способ взаимодействия между клиентом и сервером, и в качестве транспорта для него используется язык разметки XML, то в данном разделе мы немного разберемся каким образом происходит автоматическая валидация данных посредством XML-схем.

Основная задачи схемы - описать структуру данных которые мы собираемся обрабатывать. Все данные в XML-схемах делятся на простые (скалярные) и коплексные (структуры) типы. К простым типам относятся такие типы как:

  • строка,
  • число,
  • булево значение,
  • дата.

Что-то очень простое, у чего внутри нет расширений. Их антиподом являются сложные комплексные типы. Самый простой пример комплексного типа, который приходит всем в голову - объекты. Например, книга. Книга состоит из свойств: автор , название , цена , ISBN номер и т.д. И эти свойства, в свою очередь, могут быть как простыми типами, так и комплексными. И задача XML-схемы это описать.

Предлагаю далеко не ходить и написать XML-схему для нашего sms-сообщения! Ниже представлено xml-описание sms-сообщения:

<message> <phone>71239876543</phone> <text>Тестовое сообщение</text> <date>2013-07-20T12:00:00</date> <type>12</type> </message>
Схема нашего комплексного типа будет выглядеть следующим образом:

<element name="message" type="Message" /> <complexType name="Message"> <sequence> <element name="phone" type="string" /> <element name="text" type="string" /> <element name="date" type="dateTime" /> <element name="type" type="decimal" /> </sequence> </complexType>
Эта запись читается следующим образом: у нас есть переменная " message " типа " Message " и есть комплексный тип с именем " Message ", который состоит из последовательного набора элементов " phone " типа string , " text " типа string , " date " типа dateTime , " type " типа decimal . Эти типы простые и уже определены в описании схемы. Поздравляю! Мы только что написали нашу первую XML-схему!

Думаю, что значение элементов " element " и " complexType " вам стало все более-менее понятно, поэтому не будем на них больше заострять внимание и переключимся сразу же на элемент-композитор " sequence ". Когда мы используем элемент-композитор " sequence " мы сообщаем о том, что элементы включенные в него должны всегда располагаться в указанной в схеме последовательности, а также все из них являются обязательными. Но не стоит отчаиваться! В XML-схемах есть еще два элемента-композитора: " choice " и " all ". Композитор " choice " сообщает о том, что должен быть какой-то один из перечисленных в нем элементов, а композитор " all " - любая комбинация перечисленных элементов.

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

<messageList> <message> <phone>71239876543</phone> <text>Тестовое сообщение 1</text> <date>2013-07-20T12:00:00</date> <type>12</type> </message> <!-- ... --> <message> <phone>71239876543</phone> <text>Тестовое сообщение N</text> <date>2013-07-20T12:00:00</date> <type>12</type> </message> </messageList>
Схема для такого комплексного типа будет выглядеть так:

<complexType name="Message"> <sequence> <element name="phone" type="string" minOccurs="1" maxOccurs="1" /> <element name="text" type="string" minOccurs="1" maxOccurs="1" /> <element name="date" type="dateTime" minOccurs="1" maxOccurs="1" /> <element name="type" type="decimal" minOccurs="1" maxOccurs="1" /> </sequence> </complexType> <element name="messageList" type="MessageList" /> <complexType name="MessageList"> <sequence> <element minOccurs="1" maxOccurs="unbounded" name="message" type="Message"/> </sequence> </complexType>
В первом блоке идет знакомое нам декларирование комплексного типа " Message ". Если вы заметили, то в каждом простом типе, входящем в " Message ", были добавлены новые уточняющие атрибуты " minOccurs " и " maxOccurs ". Как не трудно догадаться из названия, первый ( minOccurs ) сообщает о том, что в данной последовательности должно быть минимум по одному элементу типа " phone ", " text ", " date " и " type ", в то время как следующий ( maxOccurs ) атрибут нам декларирует, что таких элементов в нашей последовательности максимум по-одному. В результате, когда мы пишем свои схемы для каких-либо данных, нам предоставляется широчайший выбор по их настройке!

Второй блок схемы декларирует элемент " messageList " типа " MessageList ". Видно, что " MessageList " представляет собой комплексный тип, который включает минимум один элемент " message ", но максимальное число таких элементов не ограничено!

На этом будем считать, что ЛикБез по схемам завершен и далее нас ждет еще одно не менее увлекательное приключение - мы будем писать свой собственный WSDL!

4 Пишем свой WSDL

Вы помните о том, что WSDL и есть наш веб-сервис? Надеюсь, что помните! Как мы его напишем, так на нем наш маленький веб-сервис и поплывет. Поэтому, предлагаю не халтурить.

Вообще, для того, чтобы у нас все работало правильно нам надо передавать клиенту WSDL-файл с правильным MIME-типом. Для этого необходимо настроить ваш веб-сервер соответствующим образом, а именно - установить для файлов с расширением "*.wsdl" MIME-тип равный следующей строке:

application/wsdl+xml
Но на практике, я обычно отправлял посредством PHP HTTP-заголовок" text/xml ":

header("Content-Type: text/xml; charset=utf-8");
и все прекрасно работало!

Хочу сразу предупредить, наш простенький веб-сервис будет иметь довольно внушительное описание, поэтому не пугайтесь, т.к. большая часть текста является обязательной водой и написав ее один раз можно постоянно копировать от одного веб-сервиса к другому!

Поскольку WSDL - это XML, то в самой первой строке необходимо прямо об этом и написать. Корневой элемент файла всегда должен называться " definitions ":

<?xml version="1.0" encoding="utf-8"?> <definitions> </definitions>
Обычно, WSDL состоит из 4-5 основных блоков. Самый первый блок - определение веб-сервиса или другими словами - точки входа.

<?xml version="1.0" encoding="utf-8"?> <definitions> <!-Определение сервиса --> <service name="SmsService"> <port name="SmsServicePort" binding="tns:SmsServiceBinding"> <soap:address location="http://localhost:80/smsservice.php" /> </port> </service> </definitions>
Здесь написано, что у нас есть сервис, который называется - " SmsService ". В принципе, все имена в WSDL-файле могут быть вами изменены на какие только пожелаете, т.к. они не играют абсолютно никакой роли.

После этого мы объявляем о том, что в нашем веб-сервисе " SmsService " есть точка входа ("port"), которая называется " SmsServicePort ". Именно в эту точку входа и будут отправляться все запросы от клиентов к серверу. И указываем в элементе " address " ссылку на файл-обработчик, который будет принимать запросы.

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

<?xml version="1.0" encoding="utf-8"?> <definitions> <!-Формат процедур веб-сервиса --> <binding name="SmsServiceBinding" type="tns:SmsServicePortType"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" /> <operation name="sendSms"> <soap:operation soapAction="" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> <!-Определение сервиса --> <service name="SmsService"> <port name="SmsServicePort" binding="tns:SmsServiceBinding"> <soap:address location="http://localhost:80/smsservice.php" /> </port> </service> </definitions>
Для этого перечисляется какие операции и в каком виде у будут вызываться. Т.е. для порта " SmsServicePort " определена привязка под именем " SmsServiceBinding ", которая имеет тип вызова " rpc " и в качестве протокола передачи (транспорта) используется HTTP. Т.о., мы здесь указали, что будем осуществлять RPC вызов поверх HTTP. После этого мы описываем какие процедуры ( operation ) поддерживаются в веб-сервисе. Мы будем поддерживать всего одну процедуру - " sendSms ". Через эту процедуру будут отправляться на сервер наши замечательные сообщения! После того, как была объявлена процедура, необходимо указать в каком виде будут передаваться данные. В данном случае указано, что будут использоваться стандартные SOAP-конверты.

После этого нам необходимо привязать процедуру к сообщениям:

<?xml version="1.0" encoding="utf-8"?> <definitions> <!- Привязка процедуры к сообщениям --> <portType name="SmsServicePortType"> <operation name="sendSms"> <input message="tns:sendSmsRequest" /> <output message="tns:sendSmsResponse" /> </operation> </portType> <!-Формат процедур веб-сервиса --> <binding name="SmsServiceBinding" type="tns:SmsServicePortType"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" /> <operation name="sendSms"> <soap:operation soapAction="" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> <!- Определение сервиса --> <service name="SmsService"> <port name="SmsServicePort" binding="tns:SmsServiceBinding"> <soap:address location="http://localhost:80/smsservice.php" /> </port> </service> </definitions>
Для этого мы указываем, что наша привязка ("binding") имеет тип " SmsServicePortType " и в элементе " portType " с одноименным типу именем указываем привязку процедур к сообщениям. И так, входящее сообщение (от клиента к серверу) будет называться " sendSmsRequest ", а исходящее (от сервера к клиенту) " sendSmsResponse ". Как и все имена в WSDL, имена входящих и исходящих сообщения - произвольные.

Теперь нам необходимо описать сами сообщения, т.е. входящие и исходящие:

<?xml version="1.0" encoding="utf-8"?> <definitions> <!-- Сообщения процедуры sendSms --> <message name="sendSmsRequest"> <part name="Request" element="tns:Request" /> </message> <message name="sendSmsResponse"> <part name="Response" element="tns:Response" /> </message> <!-- Привязка процедуры к сообщениям --> <portType name="SmsServicePortType"> <operation name="sendSms"> <input message="tns:sendSmsRequest" /> <output message="tns:sendSmsResponse" /> </operation> </portType> <!-- Формат процедур веб-сервиса --> <binding name="SmsServiceBinding" type="tns:SmsServicePortType"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" /> <operation name="sendSms"> <soap:operation soapAction="" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> <!-- Определение сервиса --> <service name="SmsService"> <port name="SmsServicePort" binding="tns:SmsServiceBinding"> <soap:address location="http://localhost:80/smsservice.php" /> </port> </service> </definitions>
Для этого мы добавляем элементы " message " с именами " sendSmsRequest " и " sendSmsResponse " соответственно. В них мы указываем, что на вход должен прийти конверт, структура которого соответствует типу данных " Request ". После чего с сервера возвращается конверт содержащий тип данных - " Response ".

Теперь надо сделать самую малость - добавить описание данных типов в наш WSDL-файл! И как вы думаете, как описываются в WSDL входящие и исходящие данные? Думаю, что вы уже все давно поняли и сказали сами себе, что при помощи XML-схем! И вы будете абсолютно правы!

<?xml version="1.0" encoding="utf-8"?> <definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://localhost/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" name="SmsWsdl" xmlns="http://schemas.xmlsoap.org/wsdl/"> <types> <xs:schema xmlns:tns="http://schemas.xmlsoap.org/wsdl/" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://localhost/"> <complexType name="Message"> <sequence> <element name="phone" type="string" minOccurs="1" maxOccurs="1" /> <element name="text" type="string" minOccurs="1" maxOccurs="1" /> <element name="date" type="dateTime" minOccurs="1" maxOccurs="1" /> <element name="type" type="decimal" minOccurs="1" maxOccurs="1" /> </sequence> </complexType> <complexType name="MessageList"> <sequence> <element minOccurs="1" maxOccurs="unbounded" name="message" type="Message"/> </sequence> </complexType> <element name="Request"> <element name="messageList" type="MessageList" /> </element> <element name="Response"> <complexType> <sequence> <element name="status" type="boolean" /> </sequence> </complexType> </element> </xs:schema> </types> <!-- Сообщения процедуры sendSms --> <message name="sendSmsRequest"> <part name="Request" element="tns:Request" /> </message> <message name="sendSmsResponse"> <part name="Response" element="tns:Response" /> </message> <!-- Привязка процедуры к сообщениям --> <portType name="SmsServicePortType"> <operation name="sendSms"> <input message="tns:sendSmsRequest" /> <output message="tns:sendSmsResponse" /> </operation> </portType> <!-- Формат процедур веб-сервиса --> <binding name="SmsServiceBinding" type="tns:SmsServicePortType"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" /> <operation name="sendSms"> <soap:operation soapAction="" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> <!-- Определение сервиса --> <service name="SmsService"> <port name="SmsServicePort" binding="tns:SmsServiceBinding"> <soap:address location="http://localhost:80/smsservice.php" /> </port> </service> </definitions>
Можно нас поздравить! Наш первый WSDL был написан! И мы еще на один шаг приблизились к достижению поставленной цели.
Далее мы разберемся с тем, что нам предоставляет PHP для разработки собственных распределенных приложений.

5 Наш первый SOAP-сервер

Ранее я писал, что для создания SOAP-сервера на PHP мы будем использовать встроенный класс SoapServer. Для того, чтобы все дальнейшие действия происходили также как и у меня, вам понадобиться немного подкрутить свой PHP. Если быть еще точнее, то необходимо убедиться, что у вас установлено расширение "php-soap". Как его поставить на ваш веб-сервере лучше всего прочитать на официальном сайте PHP (см. список литературы).

После того, как все было установлено и настроено нам необходимо будет создать в корневой папке вашего хостинга файл " smsservice.php " со следующим содержанием:

<?php /** * smsservice.php */ header("Content-Type: text/xml; charset=utf-8"); header('Cache-Control: no-store, no-cache'); header('Expires: '.date('r')); /** * Пути по-умолчанию для поиска файлов */ set_include_path(get_include_path() .PATH_SEPARATOR.'classes' .PATH_SEPARATOR.'objects'); /** * Путь к конфигурационному файлу */ const CONF_NAME = "config.ini"; /** ** Функция для автозагрузки необходимых классов */ function __autoload($class_name){ include $class_name.'.class.php'; } ini_set("soap.wsdl_cache_enabled", "0"); // отключаем кеширование WSDL-файла для тестирования //Создаем новый SOAP-сервер $server = new SoapServer("http://{$_SERVER['HTTP_HOST']}/smsservice.wsdl.php"); //Регистрируем класс обработчик $server->setClass("SoapSmsGateWay"); //Запускаем сервер $server->handle();
То, что находится выше строчки с функцией "ini_set", надеюсь, что объяснять не надо. Т.к. там определяется какие HTTP-заголовки мы будем отправлять с сервера клиенту и настраивается окружение. В строчке с "ini_set" мы отключаем кеширование WSDL-файла для того, чтобы наши изменения в нем сразу же вступали в действие на клиенте.

Теперь мы подошли к серверу! Как видим, весь SOAP-сервер занимает всего лишь три строки! В первой строке мы создаем новый экземпляр объекта SoapServer и передаем ему в конструктор адрес нашего WSDL-описания веб-сервиса. Теперь мы знаем, что он будет располагаться в корне хостинга в файле с говорящим именем " smsservice.wsdl.php ". Во второй строке мы сообщаем SOAP-серверу какой класс необходимо дергать для того, чтобы обработать поступивший с клиента конверт и вернуть конверт с ответом. Как вы могли догадаться, именно в этом классе будет описан наш единственный метод sendSms . В третьей строке мы запускаем сервер! Все, наш сервер готов! С чем я нас всех и поздравляю!

Теперь нам необходимо создать WSDL-файл. Для этого можно либо просто скопировать его содержимое из предыдущего раздела, либо позволить себе вольности и немного его "шаблонизировать":

<?php /** * smsservice.wsdl.php */ header("Content-Type: text/xml; charset=utf-8"); echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; ?> <definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://<?=$_SERVER['HTTP_HOST']?>/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" name="SmsWsdl" xmlns="http://schemas.xmlsoap.org/wsdl/"> <types> <xs:schema elementFormDefault="qualified" xmlns:tns="http://schemas.xmlsoap.org/wsdl/" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://<?=$_SERVER['HTTP_HOST']?>/"> <xs:complexType name="Message"> <xs:sequence> <xs:element name="phone" type="xs:string" minOccurs="1" maxOccurs="1" /> <xs:element name="text" type="xs:string" minOccurs="1" maxOccurs="1" /> <xs:element name="date" type="xs:dateTime" minOccurs="1" maxOccurs="1" /> <xs:element name="type" type="xs:decimal" minOccurs="1" maxOccurs="1" /> </xs:sequence> </xs:complexType> <xs:complexType name="MessageList"> <xs:sequence> <xs:element name="message" type="Message" minOccurs="1" maxOccurs="unbounded" /> </xs:sequence> </xs:complexType> <xs:element name="Request"> <xs:complexType> <xs:sequence> <xs:element name="messageList" type="MessageList" /> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="Response"> <xs:complexType> <xs:sequence> <xs:element name="status" type="xs:boolean" /> </xs:sequence> </xs:complexType> </xs:element> </xs:schema> </types> <!-- Сообщения процедуры sendSms --> <message name="sendSmsRequest"> <part name="Request" element="tns:Request" /> </message> <message name="sendSmsResponse"> <part name="Response" element="tns:Response" /> </message> <!-- Привязка процедуры к сообщениям --> <portType name="SmsServicePortType"> <operation name="sendSms"> <input message="tns:sendSmsRequest" /> <output message="tns:sendSmsResponse" /> </operation> </portType> <!-- Формат процедур веб-сервиса --> <binding name="SmsServiceBinding" type="tns:SmsServicePortType"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" /> <operation name="sendSms"> <soap:operation soapAction="" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> <!-- Определение сервиса --> <service name="SmsService"> <port name="SmsServicePort" binding="tns:SmsServiceBinding"> <soap:address location="http://<?=$_SERVER['HTTP_HOST']?>/smsservice.php" /> </port> </service> </definitions>
На этом этапе получившийся сервер нас должен устроить полностью, т.к. поступающие к нему конверты мы можем логировать и потом спокойно анализировать приходящие данные. Для того, чтобы мы могли что-либо получать на сервер, нам необходим клиент. Поэтому давайте им и займемся!

6 SOAP-клиент на подходе

Прежде всего нам надо создать файл, в котором будем писать клиент. Как обычно, мы его создадим в корне хоста и назовем " client.php ", а внутри напишем следующее:

<?php /** * /client.php */ header("Content-Type: text/html; charset=utf-8"); header('Cache-Control: no-store, no-cache'); header('Expires: '.date('r')); /** * Пути по-умолчанию для поиска файлов */ set_include_path(get_include_path() .PATH_SEPARATOR.'classes' .PATH_SEPARATOR.'objects'); /** ** Функция для автозагрузки необходимых классов */ function __autoload($class_name){ include $class_name.'.class.php'; } ini_set('display_errors', 1); error_reporting(E_ALL & ~E_NOTICE); // Заготовки объектов class Message{ public $phone; public $text; public $date; public $type; } class MessageList{ public $message; } class Request{ public $messageList; } // создаем объект для отправки на сервер $req = new Request(); $req->messageList = new MessageList(); $req->messageList->message = new Message(); $req->messageList->message->phone = '79871234567'; $req->messageList->message->text = 'Тестовое сообщение 1'; $req->messageList->message->date = '2013-07-21T15:00:00.26'; $req->messageList->message->type = 15; $client = new SoapClient( "http://{$_SERVER['HTTP_HOST']}/smsservice.wsdl.php", array( 'soap_version' => SOAP_1_2)); var_dump($client->sendSms($req));
Опишем наши объекты. Когда мы писали WSDL в нем для входящего на сервер конверта описывались три сущности: Request , MessageList и Message . Соответственно классы Request , MessageList и Message являются отражениями этих сущностей в нашем PHP-скрипте.

После того, как мы определили объекты, нам необходимо создать объект ( $req ), который будем отправлять на сервер. После чего идут две самые заветные для нас строки! Наш SOAP-клиент! Верите или нет, но этого достаточно для того, чтобы на наш сервер начали сыпаться сообщения от клиента, а также для того, чтобы наш сервер успешно их принимал и обрабатывал! В первой из них мы создаем экземпляр класса SoapClient и передаем в его конструктор адрес расположения WSDL-файла, а в параметрах явно указываем, что работать мы будем по протоколу SOAP версии 1.2. В следующей строке мы вызываем метод sendSms объекта $client и сразу же выводим в браузере результат.
Давайте запусти и посмотрим что-же у нас наконец-то получилось!

Мне с сервера вернулся следующий объект:

object(stdClass)[5] public 'status' => boolean true
И это замечательно, т.к. теперь мы точно знаем о том, что наш сервер работает и не просто работает, но еще и может возвращать на клиент какие-то значения!

Теперь посмотрим на лог, который мы предусмотрительно ведем на серверной стороне! В первой его части мы видим необработанные данные, которые поступили на сервер:

<?xml version="1.0" encoding="UTF-8"?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="http://sms-service/"> <env:Body> <ns1:Request> <ns1:messageList> <message> <phone>79871234567</phone> <text>Тестовое сообщение 1</text> <date>2013-07-21T15:00:00.26</date> <type>15</type> </message> </ns1:messageList> </ns1:Request> </env:Body> </env:Envelope>
Это и есть конверт. Теперь вы знаете как он выглядит! Но постоянно на него любоваться нам вряд ли будет интересно, поэтому давайте десереализуем объект из лог-файла и посмотрим все ли у нас хорошо:

object(stdClass)[4] public 'messageList' => object(stdClass)[5] public 'message' => object(stdClass)[6] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 1' (length=37) public 'date' => string '2013-07-21T15:00:00.26' (length=22) public 'type' => string '15' (length=2)
Как видим, объект десериализовался правильно, с чем я нас всех хочу поздравить! Далее нас ждет что-то более интересно! А именно - мы будем отправлять клиентом на сервер не одно sms-сообщение, а целую пачку (если быть точнее, то целых три)!

7 Отправляем сложные объекты

Давайте подумаем над тем, как же нам передать целую пачку сообщений на сервер в одном пакете? Наверно, самым простым способом будет организация массива внутри элемента messageList! Давайте это сделаем:

// создаем объект для отправки на сервер $req = new Request(); $req->messageList = new MessageList(); $msg1 = new Message(); $msg1->phone = '79871234567'; $msg1->text = 'Тестовое сообщение 1'; $msg1->date = '2013-07-21T15:00:00.26'; $msg1->type = 15; $msg2 = new Message(); $msg2->phone = '79871234567'; $msg2->text = 'Тестовое сообщение 2'; $msg2->date = '2014-08-22T16:01:10'; $msg2->type = 16; $msg3 = new Message(); $msg3->phone = '79871234567'; $msg3->text = 'Тестовое сообщение 3'; $msg3->date = '2014-08-22T16:01:10'; $msg3->type = 17; $req->messageList->message[] = $msg1; $req->messageList->message[] = $msg2; $req->messageList->message[] = $msg3;
В наших логах числится, что пришел следующий пакет от клиента:

<?xml version="1.0" encoding="UTF-8"?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:enc="http://www.w3.org/2003/05/soap-encoding" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="http://sms-service/"> <env:Body> <ns1:Request> <ns1:messageList> <message> <SOAP-ENC:Struct> <phone>79871234567</phone> <text>Тестовое сообщение 1</text> <date>2013-07-21T15:00:00.26</date> <type>15</type> </SOAP-ENC:Struct> <SOAP-ENC:Struct> <phone>79871234567</phone> <text>Тестовое сообщение 2</text> <date>2014-08-22T16:01:10</date> <type>16</type> </SOAP-ENC:Struct> <SOAP-ENC:Struct> <phone>79871234567</phone> <text>Тестовое сообщение 3</text> <date>2014-08-22T16:01:10</date> <type>17</type> </SOAP-ENC:Struct> </message> </ns1:messageList> </ns1:Request> </env:Body> </env:Envelope>
Что за ерунда, скажете вы? И будете правы в некотором смысле, т.к. только что мы узнали о том, что какой объект ушел от клиента, то абсолютно в том же виде он пришел к нам на сервер в виде конверта. Правда, sms-сообщения сериализовались в XML не так, как нам было необходимо - они должны были быть обернуты в элементы message , а не в Struct . Теперь посмотрим в каком виде приходит такой объект в метод sendSms :

object(stdClass)[6] public 'messageList' => object(stdClass)[7] public 'message' => object(stdClass)[8] public 'Struct' => array (size=3) 0 => object(stdClass)[9] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 1' (length=37) public 'date' => string '2013-07-21T15:00:00.26' (length=22) public 'type' => string '15' (length=2) 1 => object(stdClass)[10] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 2' (length=37) public 'date' => string '2014-08-22T16:01:10' (length=19) public 'type' => string '16' (length=2) 2 => object(stdClass)[11] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 3' (length=37) public 'date' => string '2014-08-22T16:01:10' (length=19) public 'type' => string '17' (length=2)
Что нам дает это знание? Только то, что выбранный нами путь не является верным и мы не получили ответа на вопрос - "Как нам на сервере получить правильную структуру данных?". Но я предлагаю не отчаиваться и попробовать привести наш массив к типу объект :

$req->messageList->message = (object)$req->messageList->message;
В этом случае, нам придет уже другой конверт:

<?xml version="1.0" encoding="UTF-8"?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="http://sms-service/"> <env:Body> <ns1:Request> <ns1:messageList> <message> <BOGUS> <phone>79871234567</phone> <text>Тестовое сообщение 1</text> <date>2013-07-21T15:00:00.26</date> <type>15</type> </BOGUS> <BOGUS> <phone>79871234567</phone> <text>Тестовое сообщение 2</text> <date>2014-08-22T16:01:10</date> <type>16</type> </BOGUS> <BOGUS> <phone>79871234567</phone> <text>Тестовое сообщение 3</text> <date>2014-08-22T16:01:10</date> <type>17</type> </BOGUS> </message> </ns1:messageList> </ns1:Request> </env:Body> </env:Envelope>
Пришедший в метод sendSms объект имеет следующую структуру:

object(stdClass)[7] public 'messageList' => object(stdClass)[8] public 'message' => object(stdClass)[9] public 'BOGUS' => array (size=3) 0 => object(stdClass)[10] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 1' (length=37) public 'date' => string '2013-07-21T15:00:00.26' (length=22) public 'type' => string '15' (length=2) 1 => object(stdClass)[11] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 2' (length=37) public 'date' => string '2014-08-22T16:01:10' (length=19) public 'type' => string '16' (length=2) 2 => object(stdClass)[12] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 3' (length=37) public 'date' => string '2014-08-22T16:01:10' (length=19) public 'type' => string '17' (length=2)
Как по мне, то "от перемены мест слагаемых - сумма не меняется" (с). Что BOGUS , что Struct - цель нами до сих пор не достигнута! А для ее достижения нам необходимо сделать так, чтобы вместо этих непонятных названий отображалось наше родное message. Но как этого добиться, автору пока не известно. Поэтому единственное, что мы можем сделать - избавить от лишнего контейнера. Другими словами, мы сейчас сделаем так, чтобы вместо message стал BOGUS ! Для этого изменим объект следующим образом:

// создаем объект для отправки на сервер $req = new Request(); $msg1 = new Message(); $msg1->phone = '79871234567'; $msg1->text = 'Тестовое сообщение 1'; $msg1->date = '2013-07-21T15:00:00.26'; $msg1->type = 15; $msg2 = new Message(); $msg2->phone = '79871234567'; $msg2->text = 'Тестовое сообщение 2'; $msg2->date = '2014-08-22T16:01:10'; $msg2->type = 16; $msg3 = new Message(); $msg3->phone = '79871234567'; $msg3->text = 'Тестовое сообщение 3'; $msg3->date = '2014-08-22T16:01:10'; $msg3->type = 17; $req->messageList[] = $msg1; $req->messageList[] = $msg2; $req->messageList[] = $msg3; $req->messageList = (object)$req->messageList;
Вдруг нам повезет и из схемы подтянется правильное название? Для этого посмотрим на пришедший конверт:

<?xml version="1.0" encoding="UTF-8"?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="http://sms-service/"> <env:Body> <ns1:Request> <ns1:messageList> <BOGUS> <phone>79871234567</phone> <text>Тестовое сообщение 1</text> <date>2013-07-21T15:00:00.26</date> <type>15</type> </BOGUS> <BOGUS> <phone>79871234567</phone> <text>Тестовое сообщение 2</text> <date>2014-08-22T16:01:10</date> <type>16</type> </BOGUS> <BOGUS> <phone>79871234567</phone> <text>Тестовое сообщение 3</text> <date>2014-08-22T16:01:10</date> <type>17</type> </BOGUS> </ns1:messageList> </ns1:Request> </env:Body> </env:Envelope>
Да, чуда не произошло! BOGUS - не победим! Пришедший в sendSms объект в этом случае будет выглядеть следующим образом:

object(stdClass)[6] public 'messageList' => object(stdClass)[7] public 'BOGUS' => array (size=3) 0 => object(stdClass)[8] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 1' (length=37) public 'date' => string '2013-07-21T15:00:00.26' (length=22) public 'type' => string '15' (length=2) 1 => object(stdClass)[9] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 2' (length=37) public 'date' => string '2014-08-22T16:01:10' (length=19) public 'type' => string '16' (length=2) 2 => object(stdClass)[10] public 'phone' => string '79871234567' (length=11) public 'text' => string 'Тестовое сообщение 3' (length=37) public 'date' => string '2014-08-22T16:01:10' (length=19) public 'type' => string '17' (length=2)
Как говорится - "Почти"! На этой (немного печальной) ноте предлагаю потихонечку закругляться и сделать некоторые для себя выводы.

8 Заключение

Наконец-то мы добрались сюда! Давайте определимся с тем, что вы теперь умеете делать:

  • вам по силам написать необходимый для вашего веб-сервиса WSDL-файл;
  • вы без всяких проблем можете написать свой собственный клиент способный общаться с сервером по протоколу SOAP;
  • вы можете написать свой собственный сервер общающийся с окружающим миром по SOAP;
  • вы можете отправлять массивы однотипных объектов на сервер со своего клиента (с некоторыми ограничениями).

Также, мы сделали для себя некоторые открытия в ходе нашего небольшого исследования:

  • нативный класс SoapClient не умеет правильно сериализовывать однотипные структуры данных в XML;
  • при сериализации массива в XML он создает лишний элемент с именем Struct ;
  • при сериализации объекта в XML он создает лишний элемент с именем BOGUS ;
  • BOGUS меньшее зло чем Struct из-за того, что конверт получается компактнее (не добавляются лишние namespace'ы в XML заголовке конверта);
  • к сожалению, класс SoapServer автоматически не валидирует данные конверта нашей XML-схемой (возможно, и другие сервера этого не делают).

9 Список литературы

P.S. Автор статьи хотел осветить авторизацию пакетов посредством встроенных в SOAP возможностей, но ему не удалось этого сделать по средством классов SoapServer и SoapClient. Поэтому, если у вас есть положительный опыт использования встроенной в SOAP авторизации посредством PHP, то прошу об этом написать в комментариях к статье, либо мне в личку :)

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=33796