Вступление


Что такое JSON-RPC API? Это просто один из типов API, но ещё и чёткий стандарт, чего в этой статье может и не быть(да, будет самопис).

После того как я возился с RESTful API какое-то время и сильно на него разозлился, за то, насколько он прост снаружи и может быть сложен внутри, я полез в google на поиски замены.

И я наткнулся на статью о JSON-RPC API, и меня сильно заинтересовала его концепция, настолько, что я решил реализовать свой максимально простой велосипед.

Концепция JSON-RPC API

Главная идея сего стандарта в неком объекто-ориентированном подходе.

Семантически запрос выглядит так:

{
	"api version": 0.1,
	"method": "object.method",
	"params": {
		"user id": 1234
	}
}

И это только один запрос, большая прелесть такого API в том, что их можно красиво объединять, и это даёт нам возможность использовать его для batch-запросов(загружать что-либо по частям).

То есть полный запрос может выглядеть вот таким образом.

{
	"api_v": "0.1",
	"reqs": [
		{
			"name": "my_important_request",
			"method": "user.kick_out",
			"params": {
				"id": "1234",
				"when": "now",
				...
			}
		},
		...
	]
}

Здесь api_v — это версия API, а reqs — это массив запросов. Что важно, каждый запрос имеет метод(класс.метод), параметры и имя. Имя здесь играет большую роль. Когда вам пришёл ответ с сервера, вы можете обратиться к результату запроса по его имени. Пример: Запрос с методом добавлением юзера, следует назвать «user_creating», но это вам решать ;)

Начнём писать


Первое, что нужно сделать это класс API, в моём случае он делает даже меньше, чем должен. Некоторые процессы у меня находятся отдельно от него, но сути это не меняет.

<?php
	
// @package 'api_0.1.php'
// API версия 0.1

class API
{
	
	private $last_resp; // Результат последнего запроса

	private $resp = []; // Массив запросов

	public function __call( $method, $params ) {
		// Парсим запрос, берём класс и метод
		$object = substr($method, 0, strpos($method, '.'));
		$method_name = substr($method, strpos($method, '.')+1);
		// Включаем класс 
		include_once __DIR__.'/source/'.$object.'.methods.php';
		// Исполняем метод и сохраняем результат
		$resp = $object::$method_name($params);
		if(!empty($resp))
			$this->last_resp = $resp;
		else
			$this->last_resp = null;
	}

	// Сохраняет результат последнего запроса в массив запросов с указанным ключом-именем
	pulbic function add_resp($req_name){
		if($this->last_resp === null) return false;
		$req = $this->last_resp;
		$this->resp[$req_name] = $req;
	}

	// Конечная функция, возвращает все результаты
	public function response(){
		exit ( json_encode($this->resp) );
	}

}

В коде есть комментарии, но вот краткий экскурс… Мы вызываем у API неизвестную функцию, используем магическую функцию __call для этих целей. То есть вызывая функцию «Object.method» у API ($api->{«Object.method»}), он автоматически разбивает строку на пару объект-метод и вызывает его. После чего результаты всех запросов складываются в один массив и он в формате json отправляется назад. Всё просто.

Классы


Очень важно — здесь классы храняться в папке source и вот как должен выглядеть класс

<?php
class object{
	function method($params){ /* ... */ }
}

Имя класса должно совпадать с тем, что запрашивается в запросе, тоже самое и с названием метода. Всё остальное неважно. Метод может делать что угодно и возвращать что угодно.

Но нам также нужен ещё и управляющий скрипт. Это тот самый скрипт, который будет вызываться при запросе.

<?php
	// @package api.php
	header('Content-Type: application/json'); // Устанавливаем тип данных на json

	$api_v = $_POST['api_v']; // 
	$path = __DIR__.'/APIs/api_'.$api_v.'.php'; // Составляем путь к файлу и после проверяем, что он существует
	if(!file_exists($path))
		exit; // Здесь было бы правильнее вывести ошибку
	include_once __DIR__.'/APIs/api_'.$api_v.'.php';
	// Инициализируем API
	$api = new API();

	$reqs = $_POST['reqs'];
	$reqs = @json_decode($reqs, true); // Конвертируем json в php массив (ассоциативный)

	// Проходимся по каждому запросу, вызываем определенный метод и сохраняем его результат в массив API
	foreach ($reqs as $req) {
		$method = $req['method'];
		$params = $req['params'];
		$req_name = $req['name'];
		$api->$method($params);
		$api->add_resp($req_name);
	}
	// Возвращаем все результаты
	$api->response();

Что же здесь происходит? Мы включаем наш API в зависимости от указанной в запросе версии. Декодируем json-запросы и проходимся по каждому из них. Вызываем у API метод типа «object.method» и сохраняем его результат под именем этого запроса(выше было написано, что каждый запрос имеет своё имя). После исполнения всех запросов мы возвращаем json-массив результатов… И, в принципе, всё.

Немного JS


Вот небольшой пример функции в js, которая будет делать API запросы такого типа. Написано, используя jQuery, и я дико перед вами извиняюсь за это, но так просто проще показать саму суть без лишнего.

function api_call(reqs, callback){
	// Кодируем массив(или не массив, а только один запрос, тут массив всё равно создаётся) запросов в json
	var json = JSON.stringify( (Array.isArray(reqs) ? reqs : [reqs]) );
	// Делаем POST запрос
	$.post({
		url: '/api/api.php', // путь к файлу api.php
		dataType: 'json', // Устанавливаем тип json, чтоб самим не декодировать
		data: ({ api_v: '0.1', reqs: json }), // Отправляем версию API и запросы (в json формате)
		success: function(res){
			// Сохраняем каждый запрос в массив загруженных в контексте window, чтоб использовать их в любом месте. Это совсем не обязательно
			for(var key in res){
				window.loaded[key] = res[key];
			}
			// Исполняем функцию после удачного запроса
			callback(res);
		}
	});
}

Здесь простой POST запрос, и почти нет ничего особенного, кроме того, что есть возможность указать только один запрос, а не массив, и также я сохраняю все ответы в массив загруженных, это просто так, для удобства, делать это совсем не обязательно.

Что ж, я надеюсь главная мысль вам понятна. Мне хотелось сделать простой и интуитивный API — я сделал. Я по большей части хотел показать вот такой простой подход к созданию многофункционального API.

До скорой встречи…

Комментарии (14)


  1. vdem
    01.04.2019 20:22
    +2

    и это даёт нам возможность использовать его для batch-запросов(загружать что-либо по частям)

    Вообще-то batch-запросы (пакетные) не для загрузки «чего-либо по частям», а для обработки N сущностей за один запрос, вместо одной.

    $resp = $object::$method_name($params);

    Вы уверены, что это безопасно?

    Ну и наконец, почитайте что-нибудь отсюда: раз, два, три.


  1. EvgeniiR
    01.04.2019 20:52
    +1

    Автор, взгляните, пожалуйста, на фреймворки, и огромное количество готовых пакетов на packagist.org
    Это вложение которое окупиться многократно в ближайшие же сроки


    1. Anton_Zh
      02.04.2019 04:12

      По мне так лучше сначала научиться все делать правильно без фреймворков. Для неучебного проекта, конечно, лучше подобрать фреймворк.


      1. EvgeniiR
        02.04.2019 09:58

        Научиться это хорошо, только для этого нужно захотеть учиться и знать куда развиваться


  1. SbWereWolf
    01.04.2019 20:54

    Спасибо за идею движка, для тех кто начал разбираться с программированием бэкэнда на PHP наверное позновательно. Правда Чур меня от чтения такой динами, но это ладно. Не понятно каким боком тут JSON RPC. Ожидался пример реализации, как парсить тело запрса, как формировать ответ, такие тонкости.
    А если серъёзно, то давно всё написано, бери и пользуйся. Для работы годиться любой микрофреймворк. Хотя бы роутинг будет человеческий и HTTP коды ответов можно запросто отдавать.


    1. trawl
      02.04.2019 05:15

      HTTP коды ответов можно запросто отдавать

      В JSON-RPC независим от протокола, поэтому лучше не завязывать результат на статусы


  1. AlexLeonov
    01.04.2019 22:11
    +4

    Советую автору убрать это снова в черновики, чтобы хотя бы привести код к современным стандартам PSR.
    Если нужна помощь — готов рассказать, что с кодом не так.


  1. trawl
    02.04.2019 05:12

    А чем Вас не устроил собственно JSON-RPC? Всё, описанное в статье, легко на него ложится.


    api_v? ну так сделайте разные точки входа, например /json-rpc/v1 и /json-rpc/v2


    Кстати, в базовом протоколе не было бы необходимости заворачивать в массив


    function api_call(reqs, callback){
        // Кодируем массив(или не массив, а только один запрос, тут массив всё равно создаётся) запросов в json
        var json = JSON.stringify( (Array.isArray(reqs) ? reqs : [reqs]) );
        //...
    }

    Если вдруг Вы захотите расшарить своё API в паблик, пользователям придется писать свои клиенты, вместо того, чтобы взять уже давно готовую и отлаженную библиотеку


  1. eee
    02.04.2019 05:32

    Инклюды в классах… Мне иногда кажется, что люди давно изобрели машину времени, и они из 2000-х переместились в наше время. Других причин я не могу придумать почему до сих пор пишут такой код. Есть полно фреймворков, composer, PSR стандарты, да даже ролики на ютубе.


    1. Sersoftin
      02.04.2019 08:27

      А вы внимательно посмотрите, почему именно так, а не иначе)


      1. tsukasa_mixer
        02.04.2019 09:33

        Внимательно посмотрел, ответа не нашел.


        1. trawl
          02.04.2019 09:49

          Ответ, в общем-то, на поверхности. Автор ещё не умеет в качественную разработку. Он, скорее всего ещё молод и неопытен, в нём горит желание обучаться, пройти путь от самописных CMS к разработке здорового человека.

          В целом, это не страшно, всё приходит с опытом.

          Главное, чтобы через год не стал фуллстэк-миддлом


          1. hazer_hazer Автор
            02.04.2019 10:02

            Так и есть, вы правы. Спасибо вам всем, что указали на недостатки. Я слишком мало внимания уделил самому качеству кода.


  1. crmMaster
    02.04.2019 10:21

    1.

    $api_v = $_POST['api_v']; // 
    	$path = __DIR__.'/APIs/api_'.$api_v.'.php'
    — потенциальный code injection
    2. Если будут два одинаковых запроса, данные перезатрутся
    3. Кеширование на уровне http сервера не будет работать.

    Я конечно понимаю, что для новичков концепция рест не совсем понятна, но поспрашивайте у более опытных разработчиков, чтоли. Зачем чушь то делать?