SoapServer на PHP. Пусть array всегда будет Map

в 8:32, , рубрики: array, map, php, soap, Песочница, метки: , , ,

Во время работы над серверной частью одного 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js