javascript

Простой JSON-RPC-подобный API на PHP

  • вторник, 2 апреля 2019 г. в 00:19:10
https://habr.com/ru/post/446348/
  • API
  • JavaScript
  • PHP


Вступление


Что такое 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.

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