Штатная реализация XML-RPC в PHP сделана с множеством ошибок, особенно в плане работы с кодировками. После написания ряда функций для понижения энтропии, понял, что штатная поддержка совершенно не нужна в таком виде. Т.к. она уже много лет пребывает в статусе экспериментальной и ошибки не исправляют, то имеет смысл написать собственную реализацию. Для работы ей требуется штатный пакет php-xml.
Выкладываю с целью поделить кодом и обсудить на предмет ошибок, недоработок и т.п. Код рассчитан на PHP 5.2.0 и выше (из-за применения функции strptime).
<?php
/*
(c) 2010 RXL (rxl@mail.ru)
Моя реализация сервера XML-RPC.
Основной сервис:
string $classesPath - Префикс пути для поиска файла класса.
string $classPrefix - Префикс имени класса.
string $funcPrefix - Префикс имени функции класса.
string $charset - Кодировка для ответа. Пока это условность - перекодировка не выполняется.
&array call(string &$xml) - Декодирует и выполняет запрос и возвращает закодированный
ответ.
Дополнительный сервис:
&array parseCall(string &$xml, &$method) - Разбирает запрос, устанавливает значение
вызываемого метода и возвращает структуру запроса.
&string responseXML(array &$data) - Закодировать структуру $data в ответ XML-RPC.
Имя метода состоит из трех частей, разделенных точками. Например: "test.me.getName". Первая
часть означает имя файла. Вторая - имя класса (к нему добавляется префикс). Третья - имя
статического метода класса (к нему тоже добавляется префикс).
Особенность работы с параметрами ответа (метод responseXML):
Если на первом уровне присутствует элемент "faultCode" и/или "faultString", то
создается ответ "fault", в противном случае создается "callResponse".
Имена элементов первого уровня в результат не попадают (не предусмотрено спецификацией).
Тип массива - массив или структура - определяется по индексам: в массиве индексы числовые
и идут от ноля, строго в порядке возрастания.
Принято следующее соглашение по именам элементов:
*) Имя элемента начинается с символа "*": элемент будет закодирован в base64.
*) Имя элемента начинается с символа "^": элемент будет закодирован как дата ISO8601,
согласно формату strftime() "%Y-%m-%dT%H:%M%:S%z".
*) Действие префикса в имени массива распространяется на все элементы массива (т.к. они
не имеют своего имени).
*/
class XMLRPCServer
{
public $classesPath = 'rpc_classes/';
public $classPrefix = 'rpc__';
public $funcPrefix = 'rpc__';
public $charset = 'utf-8';
private $xpath;
public function &responseToXML(&$resp)
{
if (!is_array($resp))
throw new Exception("invalid response format! Must be array.");
$xml = '<' . '?xml version="1.0" encoding="' . $this->charset . '"?' . '>';
if (!empty($resp['faultCode']) || !empty($resp['faultString']))
{
$xml .= '<methodResponse><fault><value>' .
$this->dataToXML(
array(
'faultCode' => empty($resp['faultCode']) ? -1 : $resp['faultCode'],
'faultString' => empty($resp['faultString']) ? 'Unknown error.' : $resp['faultString'],
)
) .
'</value></fault></methodResponse>';
}
else
{
$xml .= '<methodResponse><params>';
foreach ($resp as $k => $v)
{
switch (substr($k, 0, 1))
{
case '*':
$type = 'base64';
break;
case '^':
$type = 'date';
break;
default:
$type = 'auto';
break;
}
$xml .= '<param><value>' . $this->dataToXML($v, $type) . '</value></param>';
}
$xml .= '</params><methodResponse>';
}
return $xml;
}
private function &dataToXML(&$data, $type = 'auto')
{
if (is_array($data))
{
$is_assoc = false;
$n = 0;
foreach ($data as $k => $v)
{
if (!is_integer($k) || "$k" != "$n")
{
$is_assoc = true;
break;
}
$n++;
}
if ($is_assoc)
{
$xml = '<struct>';
foreach ($data as $k => $v)
{
$name = substr($k, 1);
switch (substr($k, 0, 1))
{
case '*':
$type = 'base64';
break;
case '^':
$type = 'date';
break;
default:
$type = 'auto';
$name = $k;
break;
}
$xml .= '<member><name>' . htmlspecialchars($name) . '</name><value>' .
$this->dataToXML($v, $type) . '</value></member>';
}
$xml .= '</struct>';
}
else
{
$xml = '<array><data>';
foreach ($data as $v)
$xml .= '<value>' . $this->dataToXML($v, $type) . '</value>';
$xml .= '</data></array>';
}
}
else
{
if ($type == 'date')
$xml = '<dateTime.iso8601>' . strftime('%Y-%m-%dT%H:%M%:S%z', $data) . '</dateTime.iso8601>';
elseif ($type == 'base64')
$xml = '<base64>' . base64_encode($data) . '</base64>';
elseif (is_float($data))
$xml = '<double>' . htmlspecialchars($data) . '</double>';
elseif (is_integer($data))
$xml = '<i4>' . htmlspecialchars($data) . '</i4>';
else
$xml = '<string>' . htmlspecialchars($data) . '</string>';
}
return $xml;
}
public function &parseCall(&$rawData, &$method)
{
$doc = DOMDocument::loadXML($rawData, LIBXML_NOBLANKS | LIBXML_NOENT | LIBXML_NONET);
if (!$doc)
throw new Exception("XML parse error.");
$this->xpath = new DOMXPath($doc);
$method = $this->xpath->query('/methodCall/methodName')->item(0)->nodeValue;
if (!$method)
throw new Exception("Method parse error.");
$nodes = $this->xpath->query('/methodCall/params/param/value/*');
$argv = array();
for ($i = 0; $i < $nodes->length; $i++)
{
if ($nodes->item($i)->nodeType == XML_ELEMENT_NODE)
$argv[] = $this->parseData($nodes->item($i));
}
$this->xpath = null;
return $argv;
}
private function &parseData(&$node)
{
switch ($node->nodeName)
{
case 'string':
$value = $node->nodeValue;
break;
case 'boolean':
case 'i4':
case 'int':
$value = (int)$node->nodeValue;
break;
case 'double':
$value = (double)$node->nodeValue;
break;
case 'base64':
$value = base64_decode($node->nodeValue);
break;
case 'dateTime.iso8601':
$value = strptime($node->nodeValue, '%Y-%m-%dT%H:%M%:S%z');
break;
case 'array':
$nodes = $this->xpath->query('data/value/*', $node);
$value = array();
for ($i = 0; $i < $nodes->length; $i++)
if ($nodes->item($i)->nodeType == XML_ELEMENT_NODE)
$value[] = $this->parseData($nodes->item($i));
break;
case 'struct':
$nodes = $this->xpath->query('member', $node);
$value = array();
for ($i = 0; $i < $nodes->length; $i++)
if ($nodes->item($i)->nodeType == XML_ELEMENT_NODE)
{
$name = $this->xpath->query('name', $nodes->item($i))->item(0)->nodeValue;
$value[$name] = $this->parseData($this->xpath->query('value/*', $nodes->item($i))->item(0));
}
break;
default:
throw new Exception('Unknow data type.');
}
return $value;
}
public function &call(&$rawData)
{
try
{
$argv = $this->parseCall($rawData, $method);
$method_parts = explode('.', $method);
foreach ($method_parts as $part)
if (!preg_match('/^\w+$/', $part))
throw new Exception('Wrong method name.');
if (count($method_parts) != 3)
throw new Exception('Wrong method name.');
$file = $this->classesPath . $method_parts[0] . '.php';
$class = $this->classPrefix . $method_parts[1];
$func = $this->funcPrefix . $method_parts[2];
if (!file_exists($file))
throw new Exception('Class file not found.');
eval("include_once('$file');");
if (!class_exists($class))
throw new Exception('Class not found.');
$extra = array();
$resp = call_user_func(array($class, $func), $method, $argv, $extra);
}
catch (Exception $e)
{
$resp = array(
'faultCode' => $e->getCode(),
'faultString' => $e->getMessage()
);
}
return $this->responseToXML($resp);
}
}
?>
Пример метода. Файл rpc_classes/test.php:
<?php
class rpc__me
{
public static function rpc__getName($methodName, $argv, $extra)
{
$id = $argv[0]['id'];
return array('user_' . $id);
}
}
?>
Код интерфейсного файла:
<?php
define ('CHARSET', 'utf-8');
define ('LF', "\n");
require_once('classes/xmlrpc.php');
try
{
$rpc = new XMLRPCServer();
$rpc->charset = CHARSET;
$resp = $rpc->call($HTTP_RAW_POST_DATA);
header ('Content-Type: text/xml; charset=' . CHARSET);
echo $resp, LF;
}
catch (Exception $e)
{
echo 'Error: ' . $e->getMessage() . ' (' . $e->getCode() . ')', LF;
}
?>