Если вы разработчик, и у вас есть проект на PHP, и ему наконец-то понадобилось реализовать собственное API — эта статья определенно для вас ;).
JSON-RPC v1.0 появился в 2005 году, спустя 5 лет появилась и вторая версия. В век javascript'а и мобильных приложений многие разработчики до сих пор используют свои собственные велосипеды вместо готового простого стандарта.
Почему JSON-RPC, да ещё и 2.0?
Попытаюсь выделить ключевые особенности:
- Является хоть каким-то стандартом. Богатый выбор библиотек для различных платформ.
- Быстрый легкий парсинг. JSON-RPC не такой монструозный как XML-RPC или SOAP.
- Встроенная обработка ошибок. При REST-велосипедах приходится придумывать, как правильно построить ответ и что с ним делать дальше.
- Поддержка очереди вызовов. Теперь 5 HTTP-запросов можно обернуть в один :).
- Единая точка для API. Например, /rpc-server.php сможет обрабатывать различные методы.
- Поддержка именованных и опциональных параметров при вызове методов.
Для тех, кто знаком с версией 1.0, значимыми нововведениями в 2.0 были именованные параметры и очередь вызовов.
Простой запрос/ответ выглядит следующим образом:
--> {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
<-- {"jsonrpc": "2.0", "result": 19, "id": 3}
--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}
--> [
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method": "foobar", "id": "2"}
]
<-- [
{"jsonrpc": "2.0", "result": 7, "id": "1"},
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": "2"}
]
В этом сложном «творческом» мире программирования всегда встает вопрос: взять что-то готовое или написать что-то свое?
Идеальный мир. Что нам нужно?
Как обычно, определим критерии к инструменту, который хочется найти/получить:
- Полное соответствие спецификации. С учетом этого фактора, скорее всего, не возникнет проблем с другими клиент/серверными решениями
- Однофайловая независимая реализация. Не хочется тащить за собой какой-нибудь framework а-ля Zend или PEAR
- Наличие тестов. Хоть какая-то гарантия, что open-source решение будет работать правильно ;)
- Красивая реализация внутри. Все должно быть максимально просто, понятно и логично.
- Наличие auto-discover механизма. JSON-RPC Server должен уметь отдавать мета-информацию о своих методах
- Простое подключение. Подключил файл, вызвал пару методов, все работает.
- Генерация прокси-классов клиента. Зачем писать клиент, когда у нас уже есть готовый сервер с метаданными? ;)
Для начала хватит.
Немного теории
Т.к. JSON-RPC достаточно молодой протокол, то в нем ещё есть моменты, которые до конца не утверждены. Один из них — Service Mapping Description, предложенный Dojo. SMD полностью может описать веб-сервис, начиная от его методов, заканчивая развернутыми возвращаемыми типами. К сожалению, очень мало решений поддерживает его реализацию, например Zend_Json_Server, inputEx framework (генерация формочек для тестирования) и сам Dojo framework.
Перейдем к поиску существующих решений.
Существующие реализации для PHP
Список клиентов я взял из таблички в википедии.
php-json-rpc | jsonrpc2php | tivoka | junior | json-rpc-php | JSONRpc2 | Zend Json Server |
zoServices | |
Сервер | ||||||||
Соответствие спецификации | - | Правильная поддержка Notification в Batch режиме. | + | Нет поддержки опциональных именованных параметров | + | + | + | Нет поддержки опциональных именованных параметров |
Кол-во файлов | - | 2 | >7 | 3 | 6 | 1 | >5 | 6 |
SMD-схема | - | - | - | - | - | - | + | - |
Тесты | - | + | - | + | - | + | + | - |
Внутрення реализация (1..5) | - | 4 Ручной маппинг экспортируемых функций |
3. Слишком сложная реализация для такой простой задачи | 4 | 4. Сложно | 4. Magic Inside! | 4+. Zend | 3. |
Клиент | ||||||||
Соответствие спецификации | Нет Batch и Notification | - | + | + | + | Нет Batch | - | Notificaiton отсутствуют |
Кол-во файлов | 1 | - | >7 | 4 | 2 | 1 | - | 6 |
Тесты | - | - | - | + | - | + | - | - |
Внутренняя реализация (1..5) | 4 | - | 3. Лишние шаги для вызова методов | 4 | 4 | 4+. :) | - | 3 |
Автоматическая генерация | - | - | - | - | - | - | - | - |
Умный читатель может возразить, какая разница, сколько файлов используется для реализации, все равно можно склеить все в один. Да, можно, но это дополнительное лишнее действие.
Как мы видим, нет «идеального» решения, которое бы нам подошло и которое можно спокойной использовать без напильника. Надеюсь, у других платформ дело обстоит гораздо лучше :)
Складывается общее ощущение недоделанности проектов, ни одно решение не может предложить полный цикл использования экспортируемого API (server, smd-schema, client-generation).
Ещё я не очень понимаю тех разработчиков, которые пытаются сделать из PHP, скажем Java или C#. В большинстве своем — PHP используется в схеме запрос/ответ, а не в схеме application server со своими состояниями. Скрипт — это же не скомпилированная закрытая библиотека.
Ответ на вопрос «использовать что-то готовое» или «написать свое» очевиден.
Eazy JSON-RPC 2.0
Проект на GitHub. Все требования, обозначенные ранее, реализованы :)
Server
Вариантов использования два: либо отнаследоваться от класса BaseJsonRpcServer, либо создать его экземпляр и передать в конструктор экспортируемый объект:
<?php
include 'BaseJsonRpcServer.php';
include 'tests/lib/DateTimeRpcService.php'; // тестовый пример
$server = new DateTimeRpcService(); // или new BaseJsonRpcServer( new DateTimeService() );
$server->Execute();
Таким образом, мы открыли наружу все public-методы класса DateTimeRpcService. SMD-схему можно получить через GET-параметр smd (например eazyjsonrpc/example-server.php?smd). При построении схемы учитываются phpDoc-блоки.
{"transport":"POST","envelope":"JSON-RPC-2.0","SMDVersion":"2.0","contentType":"application/json","target":"/example-server.php","services":{"GetTime":{"parameters":[{"name":"timezone","optional":true,"type":"string","default":"UTC"},{"name":"format","optional":true,"type":"string","default":"c"}],"description":"Get Current Time","returns":{"type":"string"}},"GetTimeZones":{"parameters":[],"description":"Returns associative array containing dst, offset and the timezone name","returns":{"type":"array"}},"GetRelativeTime":{"parameters":[{"name":"text","optional":false,"type":"string","description":"a date/time stringr"},{"name":"timezone","optional":true,"type":"string","default":"UTC"},{"name":"format","optional":true,"type":"string","default":"c"}],"description":"Get Relative time","returns":{"type":"string"}},"Implode":{"parameters":[{"name":"glue","optional":false,"type":"string"},{"name":"pieces","optional":true,"type":"array","default":["1","2","3"]}],"description":"Implode Function","returns":{"type":"string","description":"string"}}},"description":"Simple Date Time Service"}
Client
Вариантов использования опять же два: либо создать экземпляр класса BaseJsonRpcClient и передать ему ссылку веб-сервиса в конструкторе, либо воспользоваться генератором:
<?php
include 'BaseJsonRpcClient.php';
$client = new BaseJsonRpcClient( 'http://eazyjsonrpc/example-server.php' );
$result = $client->GetRelativeTime( 'yesterday' );
Generator
На основе SMD-схемы мы можем сгенерировать класс для работы с сервером (см. пример DateTimeServiceClient.php). Для этого вызовем генератор:
php JsonRpcClientGenerator.php http://eazyjsonrpc/example-server.php?smd DateTimeServiceClient
Результатом выполнения команды будет файл DateTimeServiceClient.php с необходимыми нам методами.
Ложка дёгтя
Негласным правилом для вызова class->method() в JSON-RPC используется class.method в качестве имени метода (через точку).
В текущей реализации такой функциональности не предусмотрено. Предполагается, что url — это экспортируемый класс, тогда вариант с точками отпадает :). Касательно клиентской части — тут всегда можно дописать, это всего-лишь PHP.
Так же в SMD есть возможность описать возвращаемые типы в виде объектов с их свойствами, но в виду сложности реализации пока этот момент мы опустим.
Тем, кто хочет найти подробную документацию, могу предложить прочитать ещё раз названия методов, phpDoc комменты к ним и исходный код Server или Client.
Лайфхаки
Что там у нас с аутентификацией?
Вариантов реализации несколько:
- Использовать HTTP Basic Auth. В клиенте достаточно добавить логин и пароль в массив $CurlOptions :)
- Использовать токены через HTTP-заголовки. Для получения токенов можно написать необходимый метод.
- Использовать токены как параметры метода.
Как быть с загрузкой файлов?
Некоторые люди предлагают странный вариант с кодированием файла в base64 и его отправкой в каком-то поле.
Более-менее нормальное решение заключается в реализации метода, который расскажет, по какому адресу можно начинать загружать файл.
--> {"jsonrpc": "2.0", "method": "send_image", "params": ..., "id": 1}
<-- {"jsonrpc": "2.0", "result": {"URL": "/exampleurl?id=12345", "maxsize": 10000000, "accepted-format":["jpg", "png"]}, "id": 1}
--> загрузим постом файл по заданному адресу.
ну и в конце можно проверить, все ли хорошо
--> {"jsonrpc": "2.0", "method": "send_done", "params": {"checksum": "1a5e8f13"}, "id": 2}
<-- {"jsonrpc": "2.0", "result": "ok"}
Обработка ошибок
Сам протокол уже предусматривает наличие объекта error с полями code, message и data. При использовании BaseJsonRpcServer внутри вызываемого метода можно кинуть Exception, в котороый передать code и data. Свой message можно добавить в массив $errorMessages по определенному code.
Экспорт объектов только с определенными полями
Тут полностью все зависит от вас, как реализуете — так и будет. Могу лишь посоветовать создать какой-нибудь класс ObjectToJsonConverter, в котором реализовать преобразования объекта в требуемый массив.
<?php
class ObjectToJsonConverter {
/**
* @param City $city
* @return array
*/
public static function GetCity( City $city ) {
return array(
'id' => $city->cityId
, 'name' => $city->title
, 'region' => $city->region->title
);
}
}
// где-то в конце экспортируемого метода
return array_map( 'ObjectToJsonConverter::GetCity', $cities );
Преобразование в объекты на стороне клиента
Тут опять же, все зависит только от вас. Например, можно создать требуемые классы и написать какой-нибудь простой конвертер обратно (идеи по конвертации можно взять в MyShowsClient.php)
Заключение
Надеюсь, после прочтения статьи, JSON-RPC не будет обделен вниманием при выборе протокола взаимодействия.
Даже после покрытия тестами > 89% кода, я могу лишь сказать:«Вроде должно работать» :)
Автор: sergeyfast