Pull to refresh

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

Reading time 29 min
Views 233K
Всем привет!
Так случилось, что в последнее время я стал заниматься разработкой веб-сервисов. Но сегодня топик не обо мне, а о том, как нам написать свой 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, то прошу об этом написать в комментариях к статье, либо мне в личку :)


P.P.S. дополнение от Mikaz
Нужно вместо массива использовать ArrayObject в совокупности с SoapVar.

Рабочий код мог бы выглядеть так:
$req = new Request();
$req->messageList = new \ArrayObject();

$msg1 = new Message();
$msg1->phone = '79871234567';
$msg1->text = 'Тестовое сообщение 1';
$msg1->date = '2013-07-21T15:00:00.26';
$msg1->type = 15;
$soap_msg1 = new \SoapVar($msg1, SOAP_ENC_OBJECT, null, null, 'Message');

$msg2 = new Message();
$msg2->phone = '79871234567';
$msg2->text = 'Тестовое сообщение 2';
$msg2->date = '2014-08-22T16:01:10';
$msg2->type = 16;
$soap_msg2 = new \SoapVar($msg2, SOAP_ENC_OBJECT, null, null, 'Message');

$msg3 = new Message();
$msg3->phone = '79871234567';
$msg3->text = 'Тестовое сообщение 3';
$msg3->date = '2014-08-22T16:01:10';
$msg3->type = 17;
$soap_msg3 = new \SoapVar($msg3, SOAP_ENC_OBJECT, null, null, 'Message');

$req->messageList->append($soap_msg1);
$req->messageList->append($soap_msg2);
$req->messageList->append($soap_msg3);

Tags:
Hubs:
+28
Comments 26
Comments Comments 26

Articles