Во время работы над серверной частью одного iphone приложения, всплыла любопытная особенность Zend_Soap_Server. Приводила она к спонтанно (на первый взгляд) возникающим ошибкам при возвращении php-ных массивов. У нас выявление и отладка заняли несколько человеко-часов, и, возможно, данная статья позволит кому-то те же самые несколько часов сэкономить.
Стоит отметить, что Zend_Soap_Server — это несложная обертка над встроенным SoapServer, и описанный ниже эффект наблюдается не только при использовании ZF, но и при работе с SoapServer напрямую.
Метод, в котором мистическим образом возникала ошибка, занимается тем, что находит цены на отели по заданным клиентом параметрам. Возвращаются они вместе с некоторой дополнительной информацией и итоговая структура выдачи выглядит примерно так:
<?php
$result = array(
'info' => array(
'key_1' => 'value_1',
...,
'key_n' => 'value_n'
),
'prices' => array(
id_1 => price_1,
...
id_m => price_m
)
);
return $result;
?>
Из важных особенностей — цены на сервере упорядочиваются по возрастанию; id_k — это целые неотрицательные числа, (идентификаторы отелей); если по заданным критериям невозможно найти ни одной актуальной цены, то возвращается сообщение об ошибке (другая структура).
Все проблемы, как выяснилось из анализа логов запросов, были связаны с массивом prices. В подавляющем большинстве случаев он не был массивом в “классическом” понимании. То есть его ключи не были идущими подряд целыми, начинающимися с 0. Подобные данные SoapServer считает (справедливо) типом Map и конвертирует (если залезть в raw xml ответа) к виду
<item>
<key xsi:type="xsd:string">prices</key>
<value xsi:type="ns2:Map">
<item>
<key xsi:type="xsd:int">100</key>
<value xsi:type="xsd:int">150</value>
</item>
<item>
<key xsi:type="xsd:int">2</key>
<value xsi:type="xsd:int">300</value>
</item>
<item>
<key xsi:type="xsd:int">1078</key>
<value xsi:type="xsd:int">306</value>
</item>
</value>
</item>
И лишь изредка prices оказывались действительно “классическим” массивом. Например, такая ситуация возникала, когда единственным доступным вариантом был отель с id = 0. Подобные данные SoapSever считает (опять же, справедливо) типом Array и приводит к виду
<item>
<key xsi:type="xsd:string">prices</key>
<value enc:itemType="xsd:int" enc:arraySize="1" xsi:type="enc:Array">
<item xsi:type="xsd:int">420</item>
</value>
</item>
Возможно, ошибка проявилась бы раньше, если бы метод возвращал аналогичную структуру для “пустых” результатов. Эта ситуация более распространена, логично выделяется как отдельный случай при тестировании, а SoapServer пустой массив тоже считает типом Array:
<item>
<key xsi:type="xsd:string">prices</key>
<value enc:itemType="xsd:anyType" enc:arraySize="0" xsi:type="enc:Array"/>
</item>
Таким образом, в зависимости от данных, наш сервер возвращал клиенту разные типы — в основном Map, но иногда Array. А клиент всегда ожидал увидеть Map и падал при получении Array. Проблема была выявлена, но пока еще не решена.
В качестве возможных вариантов решения были предложены
1) изменение на стороне клиента — чтобы он умел разбирать оба случая;
2) добавить в массив prices фейковый строковый ключ и пропускать его на клиенте при разборе;
3) добиться того, чтобы сервер всегда выдавал тип Map, менее “кривым” способом, чем пункт 2.
Третий вариант был сочтен предпочтительным и после качественного и не слишком короткого гугления решение было найдено на stackoverflow.
Массив prices нужно обернуть в SoapVar с указанием типа APACHE_MAP
<?php
$result = ...;
$result['prices'] = new SoapVar($result['prices'], APACHE_MAP);
return $result;
?>
и только после этого возвращать клиенту. При наличии такой обертки SoapServer уже не смотрит на реальные данные для определения типа, а всегда возвращает Map — и для пустых массивов:
<item>
<key xsi:type="xsd:string">prices</key>
<value xsi:type="ns2:Map"/>
</item>
и для “классических” непустых:
<item>
<key xsi:type="xsd:string">prices</key>
<value xsi:type="ns2:Map">
<item>
<key xsi:type="xsd:int">0</key>
<value xsi:type="xsd:int">100</value>
</item>
<item>
<key xsi:type="xsd:int">1</key>
<value xsi:type="xsd:int">200</value>
</item>
<item>
<key xsi:type="xsd:int">2</key>
<value xsi:type="xsd:int">300</value>
</item>
</value>
</item>
Несмотря на итоговую простоту решения и то, что оно было найдено методом Google'а, сама ситуация показалась мне интересной, а ошибка вполне типичной и достойной описания, что и привело к написанию этой статьи.
Для полноты информации: софт, работающий на сервере: PHP 5.3, Zend Framework 1.11, на других версиях я не проверял, хотя предполагаю, что все должно быть аналогично.
Автор: rdaemon