JSON-RPC 2.0 и PHP

в 5:50, , рубрики: php, smd, Веб-разработка, веб-сервисы, метки: , ,

Если вы разработчик, и у вас есть проект на 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.

Лайфхаки

Что там у нас с аутентификацией?

Вариантов реализации несколько:

  1. Использовать HTTP Basic Auth. В клиенте достаточно добавить логин и пароль в массив $CurlOptions :)
  2. Использовать токены через HTTP-заголовки. Для получения токенов можно написать необходимый метод.
  3. Использовать токены как параметры метода.

Как быть с загрузкой файлов?

Некоторые люди предлагают странный вариант с кодированием файла в 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

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


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