24 ноября 2017, пятница  

PHPworld.ru - все о программировании на PHP, основы языка PHP, особенности PHP5 Система продвижения сайтов нового поколения
PHPworld.ru php php 5 SQLite Статьи Книги Форум PHP Гостевая книга Ссылки по PHP

Статьи


 

Работа с Веб-сервисами .Net на PHP

Никитин Иван
ZCE, MCSD
ivan[at]nikitin.org

В статье http://www.phpworld.ru/articles/soapclient.php Леонид Лукин очень хорошо показал как можно просто и легко создать клиента Веб-сервиса для вызова удаленной службы и получения данных от внешнего сервера. Я лишь хотел бы дополнить указанную статью небольшими практическими примерами.

Веб-сервисы Microsoft .Net

В последнее время Веб-сервисы получили достаточно широкое распространение, и применяются в самых разных качествах: от простого предоставления справочных данных в Сеть, например, данных о прилете самолетов (Веб-сервис аэропорта Шереметьево), курсов валют и драгоценных металлов (Центральный Банк России) до работы с кредитными карточками (процессинговый центр Assist) и онлайновых переводов текста (Веб-сервис компании Prompt). Еще больше существует корпоративных Веб-сервисов, которые решают самые разные корпоративные задачи.

Я думаю, что такому широкому распространению Веб-сервисов немало способствовала технология Microsoft .Net. Дело в том, что создать Веб-сервис в той же Microsoft Visual Studio .Net 2003 невероятно просто. Грубо говоря, достаточно написать свой класс, унаследованный от класса System.Web.Services.WebService и объявить его методы как Веб-методы. И все! Компилируйте, переносите на рабочий сервер и ваш новый Веб-сервис готов к работе!

Так же просто написать и клиента для любого Веб-сервиса. В состав средств разработки Microsoft входит утилита wsdl.exe. Достаточно ей указать адрес WSDL документа, желаемый язык разработки (C#, VB.Net) и она сгенерирует вам код класса, который является прокси-классом для указанного Веб-сервиса, то есть его клиентом. То есть вы получаете готовый к использованию класс с тем же набором методов, что и Веб-сервис. Любое обращение к этим методам автоматически транслируется Веб-сервису и ответ Веб-сервиса возвращается как результат. Приложение даже не знает, что оно работает с Веб-сервисом, для приложения это просто обращение к локальному  классу.

Все это очень значительно упрощает разработку и поэтому большинство Веб-сервисов, по крайней мере, в России, написаны именно на каком-нибудь языке .Net и работают именно на платформе .Net.

Узнать Веб-сервис .Net достаточно просто. Во-первых, файл, к которому происходит обращение, имеет расширение asmx, а во-вторых, при простом обращении к этому файлу (метод GET), Веб-сервис .Net генерирует HTML страничку с описанием своих методов, что тоже достаточно удобно. К тому же, Веб-сервисы .Net могут отвечать не только на запросы SOAP, но и на обычные запросы POST и GET. Но это больше дополнительные возможности, «фичи», которые к тому же, могут быть отключены разработчиком, и нами здесь не рассматриваются.

Однако при попытке использования Веб-сервисов .Net разработчик PHP может столкнуться с рядом проблем, решение которых мы здесь и покажем. В качестве примера мы создадим на PHP клиента Веб-сервиса Центрального Банка России и будем получать от него актуальные курсы валют на любую дату.

Сам Веб-сервис находится здесь: http://www.cbr.ru/DailyInfoWebServ/DailyInfo.asmx

Ну-с, приступим…

WSDL

Написание любого Веб-сервиса начинается с изучения WSDL документа – описания самого Веб-сервиса, его методов, параметров методов и типов данных. Для Веб-сервисов .Net WSDL описание генерируется автоматически, достаточно обратится к Веб-сервису с параметром «?wsdl» в строке URL:
http://www.cbr.ru/DailyInfoWebServ/DailyInfo.asmx?wsdl

Если внимательно изучить данный документ, будет ясно происхождение первой проблемы, с которой сталкиваются разработчики PHP при попытке вызова Веб-сервиса .Net с помощью класса SoapClient, а именно, как передавать параметры в метод Веб-сервиса. Вторая же проблема, как читать ответ Веб-сервиса, также объясняется документом WSDL.

Для получения данных о курсах валют вы будем использовать метод Веб-сервиса GetCursOnDateXML, который, судя, по описанию, должен реализовать  «Получение ежедневных курсов валют (как XMLDocument)». Найдем этот метод в WSDL описании:

<operation name="GetCursOnDateXML">
  <documentation>
     Получение ежедневных курсов валют (как XMLDocument)
  </documentation>
  <input message="s0:GetCursOnDateXMLSoapIn" />
  <output message="s0:GetCursOnDateXMLSoapOut" />
</operation>

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

<s:element name="GetCursOnDateXML">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="1" maxOccurs="1" name="On_date" type="s:dateTime" />
    </s:sequence>
  </s:complexType>
</s:element>

<s:element name="GetCursOnDateXMLResponse">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="0" maxOccurs="1" name="GetCursOnDateXMLResult">
        <s:complexType mixed="true">
          <s:sequence>
            <s:any />
          </s:sequence>
        </s:complexType>
      </s:element>
    </s:sequence>
  </s:complexType>
</s:element>

Обратите внимание, что оба сообщения являются complexType (комплексный тип), который состоит из sequence (последовательности) элементов. Если сказать по-другому, то это объекты с наборами свойств.

Дело в том, что для унификации описания своих методов, платформа .Net использует прием, при котором предполагается, что любой метод получает единственный параметр при вызове, и этот параметр – объект, у которого определены свойства с именами аргументов вызова – это, собственно, сами аргументы вызова. При ответе метод вернет вам объект, у которого есть единственное свойство с именем названиеМетодаResult. Тип этого свойства зависит от описания Веб-метода, точнее от того, что он вам возвращает. Если это простой тип – то он будет определен как скалярный тип (строка, число, логический), если сложный (например, массив данных), то он будет определен как ассоциативный массив с элементом any или отдельной схемой данных (например, если Веб-сервис возвращает набор данных System.Data.Dataset).

Именно это и надо учитывать при реализации клиента на PHP. Однако, это не так сложно, как может показаться на первый взгляд. При вызове метода мы должны просто создать ассоциативный массив с любым именем, заполнить его элементами, для которых имена – это названия параметров, а значения – значения этих параметров. И именно этот массив и передавать Веб-сервису. Ответ же читать из свойства названиеМетодаResult, если этот простой (скалярный тип) или из свойства названиеМетодаResult->any, если в WSDL описании вы видите строки:

<s:complexType mixed="true">
  <s:sequence>
    <s:any />
  </s:sequence>
</s:complexType>

Если же в описании ответа вы видите что-то вроде этого:
<s:complexType>
  <s:sequence>
    <s:element ref="s:schema" /> 
  </s:sequence>
</s:complexType>

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


Клиент Веб-сервиса Центробанка России

Итак, нам для этого понадобится PHP5 , с подключенным модулем SOAP. Создадим класс CBR:

class CBR
{
 // WSDL службы Центробанка
 const WSDL = "http://www.cbr.ru/DailyInfoWebServ/DailyInfo.asmx?WSDL";
 // Экземпляр класса SoapClient
 protected $soap;
 // Первоначальная инициализация
 public function __construct()
 {
  $this->soap = new SoapClient(CBR::WSDL);
 }
}

Вызов метода Веб-сервиса

Для того, чтобы вызывать метод GetCursOnDateXML, нам необходимо передавать ему параметр On_date, причем этот параметр имеет тип dateTime. Это видно из описания WSDL:
<s:element name="GetCursOnDateXML">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="1" maxOccurs="1" name="On_date"
         type="s:dateTime" />
    </s:sequence>
  </s:complexType>
</s:element>

Представление даты и времени при SOAP вызовах описано в документе http://www.w3.org/TR/xmlschema-2/#dateTime, мы же напишем функцию, которая будет делать такое преобразование:

// Параметры:
//   $timeStamp - дата/время в формате UNIX
//   $withTime - необязательно если true,
//                то преобразование вместе со временем суток,
//                иначе только дата
function getSOAPDate($timeStamp, $withTime = false)
{
 $soapDate = date("Y-m-d", $timeStamp);
 return ($withTime) ?
  $soapDate .  "T" . date("H:i:s", $timeStamp) :
  $soapDate . "T00:00:00";
}

Теперь, собственно и можно произвести вызов Веб-сервиса

// Метод возвращает XML строку с результатами вызова Веб-службы
// Параметры:
//   $date - дата, на которую производится запрос
function getXML($date)
{
 // Строка даты, на которую производится вызов
 $currentDate = $this->getSOAPDate($date);
 // Формируем массив параметров
 $params["On_date"] = $currentDate;
// Вызов Веб-службы
 $response = $this->soap->GetCursOnDateXML($params);
// Возвращаем результат
 return $response->GetCursOnDateXMLResult->any;
}

Обратите внимание, параметр вызова On_date формируется как элемент ассоциативного массива $params, который передается Веб-сервису. Ответ (XML строку) мы считываем из свойства $response->GetCursOnDateXMLResult->any, что определяется WSDL описанием.

В результирующем коде я немного усложнил этот метод: я сохраняю полученную дату в свойстве класса, и ответ сервиса также сохраняю, а перед вызовом просто проверяю, был ли предыдущий вызов на эту же дату, и если да, то реальный вызов не производится, а возвращается просто предыдущий (сохраненный) результат. Это сделано для ускорения работы: каждый вызов Веб-сервиса – это довольно медленное сетевое взаимодействие с удаленным сервером. Кроме того, настоятельно рекомендую включить кэширование WSDL документов для модуля SOAP. Это можно сделать в настройках php.ini или в файле .htaccess:

[soap]
; Enables or disables WSDL caching feature.
soap.wsdl_cache_enabled=1
; Sets the directory name where SOAP extension will put cache files.
soap.wsdl_cache_dir="C:WindowsTemp"
; (time to live) Sets the number of second while cached file will be used
; instead of original one.
soap.wsdl_cache_ttl=86400

В этом случае при каждой инициализации вашего класса (при создании экземпляра класса SoapClient) PHP не будет запрашивать WSDL с удаленного сервера, а будет брать его из кеша, что также положительно скажется на быстродействии.


Разбор XML строки – ответа Веб-сервиса

Итак, мы получили строку XML, как результат вызова Веб-сервиса Центробанка России. В этом XML документе содержатся данные о курсах валют на указанную дату. Если просто напечатать результат на экране (или вывести в файл), то можно увидеть его структуру:
<ValuteData xmlns="">
<ValuteCursOnDate>
    <Vname>Австралийский доллар</Vname>
    <Vnom>1</Vnom>
    <Vcurs>20.8448</Vcurs>
    <Vcode>36</Vcode>
    <VchCode>AUD</VchCode>
 </ValuteCursOnDate>
   . . .
</ValuteData>

Как говорится, пояснения излишни. Попробуем разобрать его и получить данные для требуемой валюты. Для разбора этого XML документа я воспользовался модулем SimpleXML, который входит в PHP5. Модуль SimpleXML я выбрал по двум причинам: во-первых, он быстрый и удобный для простого анализа XML документов, во-вторых, у него есть очень удобный метод XPath, который позволяет выбрать узлы документа по XPath выражению. Конечно же, все тоже самое можно сделать и стандартным DOM анализом, но с помощью SimpleXML код получается короче. Напишем функцию выборки курса валюты по ее коду:

// Параметры:
//   $currencyCode - код валюты: USD, EUR и т.п.
//   $date - необязательно, дата, на которую производится запрос,
//           0 - сегодня
function getRate($currencyCode, $date = 0)
{
 if (!$date) $date = time();
 $xml = simplexml_load_string($this->getXML($date));
 $xPath = "/ValuteData/ValuteCursOnDate[VchCode=''$currencyCode'']";
 $result = $xml->xpath($xPath);
 if (count($result) == 0) return 0;
 return $result[0]->Vcurs / $result[0]->Vnom;
}

Видно, что мы получаем два параметра: код валюты и дату (дата необязательна), производим запрос с помощью функции getXML и ответ загружаем в объект SimpleXML. Далее, формируем XPath выражение для выборки узла ValuteCursOnDate по значению узла VchCode. Как результат метод $xml->xpath возвращает массив выбранных узлов. Если массив пуст – значит такого кода валюты в ответе нет. Если же код есть, то 0-й элемент массива – это наша валюта. Возьмем ее курс и разделим на номинал.

Все, текущий курс получен.

В конечном коде я еще написал метод, который формирует в классе ассоциативный массив кодов валют и их названий. Данный метод вызывается в конструкторе класса, что стразу же создает этакий справочник по кодам валют, как свойство класса (например, $cbr-> currencyCodes). Им удобно пользоваться например, вот для такой страницы:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
 "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
 <title>Курсы валют</title>
</head>
<body>
<?php
 require("cbr.class.php");
 $currency = substr(trim(strtoupper($_GET["currency"])), 0, 3);
 $date = @strtotime($_GET["date"]);
 if (!$currency) $currency = "USD";
 if ($date < 1) $date = time();
 $result = "";
 $cbr = new CBR();
 if ($_SERVER["QUERY_STRING"])
  $result = $cbr->getRate($currency, $date);
?>
 <h1>Курсы валют</h1>
 <form action="" method="get">
  <div>
   <label>Валюта</label>
   <select name="currency">
    <?php
     foreach($cbr->currencyCodes as $code => $name)
     {
      $selected = ($currency == $code) ?
        " selected" : "";
      echo "<option value="$code"$selected>$name</option>n";
     }
    ?>
   </select>
  </div>
  <div>
   <label>Дата</label>
   <input type="text" name="date"
    value="<?php echo date("Y-m-d", $date) ?>">
  </div>
  <input type="submit" value="Показать курс"> 
 </form>
<?php if ($result) echo "<div>", $result, "</div>" ?>
</body>
</html>

Как мы видим, использование класса свелось к простому вызову его метода getRate. Например, получить курс доллара на сегодня можно так:
. . .
$cbr = new CBR();
$usd = $cbr->getRate("USD");
. . .

Таким образом, использование этого класса позволит теперь вашем наработкам всегда легко получать актуальные курсы валют по данным Центробанка РФ. Приведем здесь полный код класса CBR.

Класс CBR. Полный код

<?php
/*
** Класс CBR
** Производит запрос курсов валют с Веб службы Центробанка России
*/
class CBR
{
 // WSDL службы Центробанка
 const WSDL = "http://www.cbr.ru/DailyInfoWebServ/DailyInfo.asmx?WSDL";
 // Экземпляр класса SoapClient
 protected $soap;
 // Дата запроса в формате SOAP
 protected $soapDatedate = "";
 // XML ответа Веб-службы
 protected $soapResponse = "";
 // Ассоциативный массив с кодами валют
 public $currencyCodes = array();

 // Первоначальная инициализация
 public function __construct()
 {
  $this->soap = new SoapClient(CBR::WSDL);
  $this->getCurrencyCodes();
 }

 // Метод формирует строку с датой временем для SOAP вызода
 // http://www.w3.org/TR/xmlschema-2/#dateTime
 // Параметры:
 //   $timeStamp - дата/время в формате UNIX
 //   $withTime - необязательно если true,
      //                то преобразование вместе со временем суток,
 //                иначе только дата
 protected function getSOAPDate($timeStamp, $withTime = false)
 {
  $soapDate = date("Y-m-d", $timeStamp);
  return ($withTime) ?
   $soapDate .  "T" . date("H:i:s", $timeStamp) :
   $soapDate . "T00:00:00";
 }
 
 // Метод возвращает XML строку с результатами вызова Веб-службы
 // Параметры:
 //   $date - дата, на которую производится запрос, 0 - сегодня
 protected function getXML($date)
 {
  // Строка даты, на которую производится вызов
  $currentDate = $this->getSOAPDate($date);
  // Если предыдущий запрос службы был не на эту дату...
  if ($currentDate != $this->soapDate)
  {
   // Вызов Веб-службы
   $this->soapDate = $currentDate;
   $params["On_date"] = $currentDate;
   $response = $this->soap->GetCursOnDateXML($params);
   $this->soapResponse =
                    $response->GetCursOnDateXMLResult->any;
  }
  return  $this->soapResponse;
 }
 
 // Метод возвращает курс указанной валюты
 // Параметры:
 //   $currencyCode - код валюты: USD, EUR и т.п.
 //   $date - необязательно, дата, на которую производится запрос,
      //           0 - сегодня
 public function getRate($currencyCode, $date = 0)
 {
  if (!$date) $date = time();
  $xml = simplexml_load_string($this->getXML($date));
  $xPath = "/ValuteData/ValuteCursOnDate[VchCode=''$currencyCode'']";
  $result = $xml->xpath($xPath);
  if (count($result) == 0) return 0;
  return $result[0]->Vcurs / $result[0]->Vnom;
 }
 
 // Метод заполняет массив кодов валют, по данным на текущий день
 protected function getCurrencyCodes()
 {
  $xml = simplexml_load_string($this->getXML(time()));
  $xPath = "/ValuteData/ValuteCursOnDate";
  $allCurrencies = $xml->xpath($xPath);
  foreach ($allCurrencies as $currency)
  {
   $code = trim($currency->VchCode);
   $name = trim(iconv("UTF-8", "windows-1251",
                          $currency->Vname));
   $this->currencyCodes[$code] = $name;
  }
 }
}
?>

Улучшенный клиент Веб-сервиса Центробанка России

Полученный нами класс очень удобен для самых различных сценариев, например, для назначения цены в рублях по цене в долларах или евро при расчете корзины электронного магазина. Однако, несложно заменить, что на обращение к ЦБ РФ PHP тратит некоторое время. Замедления связаны с медленным сетевым взаимодействием, ведь на каждую новую дату класс CBR производит новый запрос Веб-службы ЦБ РФ. При большой нагрузке на ваш сайт эти замедления могут резко снизить скорость ваших сценариев. Сейчас мы постараемся исправить ситуацию. Ввиду того, что ставки валют ЦБ РФ не меняются чаще чем раз в день, мы закешируем ответ Веб-службы в локальном файле с тем, чтобы после одного обращения к какой либо дате, эти данные были бы доступны нам и при следующих вызовах. Так как чтение локального файла намного быстрее, чем обращение к Веб-севрису, то этот прием самым положительным образом скажется на быстродействии нашего кода. 

Класс CachedCBR. Полный код

<?php
/*
** Класс CachedCBR
** Производит запрос курсов валют с Веб службы Центробанка России
** Запрос на указанную дату кешируется
*/
require("cbr.class.php");
class CachedCBR extends CBR
{
 // Временная папка для сохранения файлов в кеше
 public $tempFolder = "";
 
 // Метод возвращает XML строку с результатами вызова Веб-службы
 // Параметры:
 //   $date - дата, на которую производится запрос, 0 - сегодня  
 protected function getXML($date)
 {
  // Вычислим имя файла
  $cacheFile = md5($this->getSOAPDate($date)) . ".xml";
  if ($this->tempFolder)
    $cacheFile = $this->tempFolder . $cacheFile;
  if (!file_exists($cacheFile))
  {
   // Файла в кеше нет - делаем запрос
   $result = parent::getXML($date);
   file_put_contents($cacheFile, $result);
   return $result;
  }
  else
  {
   // Файл в кеше есть - прочитаем его
   return file_get_contents($cacheFile);
  }
 }
}
?>

Легко заметить, мы просто перекрыли метод getXML новой реализацией. Сначала мы вычисляем имя временного файла в кеше, во избежание конфликта по именам, в качестве имени мы берем значение хеша MD5 от указанной даты. Это позволит генерировать нам любое количество временных файлов с практически уникальными именами, зависящими только от указанной даты. Далее, мы просто проверяем, есть ли такой файл во временной папке (по умолчанию, в текущей). Если такой файл не найден, значит, запрос на текущую дату еще не производился и вы выполняем этот запрос обращением к Веб-сервису. Ответ запроса сохраняется в этом временном файле. Если файл найден, мы просто считываем его и возвращаем как результат вызова метода. Вот и все.

Этот нехитрый прием резко увеличил быстродействие нашего класса, ведь обращение к ЦБ РФ осуществляется только один раз на одну дату: первый же запрос на какую-то дату будет закеширован и будет возвращаться намного быстрее, чем ранее. Конечно же, время от времени кеш нужно чистить, стирая временные файлы, но это вы уже реализуйте сами. Кому-то покажется удобным чистить кеш раз в день, кому-то раз в неделю…

Выводы

Итак, вызывать методы Веб-сервисов .Net из сценариев PHP очень просто. Нужно лишь просто помнить, что все параметры, которые требует метод Веб-сервиса нужно передавать в виде ассоциативного массива с элементами, имена которых соответствуют именам аргументов Веб-метода. Ответ же нужно читать из свойства названиеМетодаResult объекта, который вам вернул метод. И еще раз, внимательно изучайте WSDL описание Веб-сервиса.

Заключение

Таким образом, мы на практическом примере увидели с вами простоту использования Веб-сервисов. Я был бы вам очень признателен за комментарии к этой статье, которые вы можете оставить в форуме данного сайта. Эти комментарии помогут мне оценить, насколько актуальна для вас эта информация, и возможно написать следующие статьи, например, как сделать Windows клиента (локальное приложение Windows, или приложение для КПК) для Веб-сервиса, написанного на PHP, или, скажем, разбор сложных классов Dataset, полученных от каких-нибудь Веб-служб .Net

За сим остаюсь искренне Ваш,
Иван Никитин

Обсудить статью на форуме

Дата публикации: 19.02.06
Последнее обновление: -
Просмотров: 54619

Copyright © PHP World - Леонид Лукин, 2004-2008.

Главная PHP World | PHP | PHP 5 | SQLite | Статьи по PHP | Книги по PHP | Ссылки по PHP
Рекомендуем посетить: афиша концертов