Свою первую апишку я написал лет 7-8 назад и это был первый блин. В целом этот блин прошел кучу испытаний и модернизаций и получилось, что то вполне вменяемое. Даже сейчас я понимаю, что это ядро актуально и его можно развивать дальше и оптимизировать к текущим реалиям (пока нет подходящего проекта).
Как можно догадаться это было RPC. Наверно стоит начать с того, что я выделил ряд слоев (какие то можно опустить, какие то добавить).
получение запроса в текст
конвертация текста в ассоциативный массив
конвертация ассоциативного массива в класс запроса (понять имя метода, данные метода, токен и другие доп. данные (язык, версия, ...))
валидация наличия метода
права доступа (не доделал)
валидация (и преобразование данных в объект)
DI контейнер (не доделал на php)
логика метода (возвращает статус ответа и данные)
конвертируем статус объекта и данные в структуру ответа
преобразовываем структуру ответа в текст
Таким образом у нас получилось 10 слоев которые нам надо реализовать и которые нам кажутся очень сложными (наверно по этой причине RPC называют сложной и это очень сильно отталкивает). На самом же деле большая часть этих слоев уходит в ядро и вам не нужно их трогать. В моем случае остается зарегистрировать метод, описать схему валидации (иногда дописать метод валидации и преобразования) и написать саму логику. До схемы ответа, обычно руки доходят в последний момент...
Отдельным слоем можно добавить работу с файлами, их валидацию. У меня этот кейс встречал не так часто, по этому идем дальше.
Примеры написания методов
Простой пример метода на Go
Я не являюсь супер go разработчиком и тут было конвертировано RPC ядро с php на go за неделю. Народ на go очень специфичный и я не думаю поймать бурю аплодисментов, но пример думаю стоит вставить.
package methodGroup2
import (
"project-my-test/example/rpcApp/methodRequestShemaItem"
"project-my-test/src/rpc"
"project-my-test/src/rpc/rpcInterface"
"project-my-test/src/rpc/rpcStruct"
)
type MethodMyTest struct {
rpc.RpcMethod
Data struct {
Name *string
Email *string
}
Logger rpcInterface.Logger
}
func (r *MethodMyTest) GetRequestSchema() map[string]rpcStruct.ReformSchema {
rs := make(map[string]rpcStruct.ReformSchema)
rs["Name"] = methodRequestShemaItem.GROUP2_NAME()
rs["Email"] = methodRequestShemaItem.GROUP2_EMAIL()
return rs
}
func (r *MethodMyTest) Run() rpcInterface.Response {
r.Logger.Info("test Info")
r.Logger.Warning("test Warning")
r.Logger.Error("test Error")
r.Logger.Debug("test Debug")
r.Response.SetData("test::string", "string")
r.Response.SetData("test::int", 20)
r.Response.SetData("test::Name", r.Data.Name)
r.Response.SetData("test::Email", r.Data.Email)
r.Response.GetError().SetCode("ERROR")
return r.Response
}
Пример авторизации на php
<?php
namespace CustomRpc\Method\RpcUser;
class RpcUserAuth extends \Oploshka\Rpc\Method {
public static function description(){
return <<<DESCRIPTION
Авторизация пользователя (пользователь должен подтвердить почту!)
Пример объекта auth
{ "login": "test@mail.ru", "password": "12345678" }
При успешном ответе вернется session.
DESCRIPTION;
}
public static function validate(){
return [
'auth' => ['type' => 'array', 'req' => true, 'validate' => [
'login' => ['type' => 'string' , 'validate' => [], 'req' => true ],
'password' => ['type' => 'string', 'validate' => [], 'req' => true ],
] ],
];
}
public function run(){
$RpcUser = \CustomRpc\EntityQuery\RpcUserQuery::auth($this->Data['auth']['login'], $this->Data['auth']['password']);
if(!$RpcUser){
// TODO: add error auth count
$this->Response->setError('ERROR_LOGIN_PASSWORD'); return;
}
$userSessionToken = \CustomRpc\EntityQuery\RpcUserSessionQuery::addNewUserSession($RpcUser);
$this->Response->setData('session', $userSessionToken);
$this->Response->setError('ERROR_NO');
}
public static function return(){
return [
'session' => ['type' => 'string' , 'validate' => [], 'req' => true ],
];
}
}
Пример обновления данных пользователя
<?php
namespace CustomRpc\Method\RpcUser;
class RpcUserInfoUpdate extends \Oploshka\Rpc\Method {
public static function description(){
return <<<DESCRIPTION
Обновление своих данных
gender = 'NULL' 'MALE' 'FEMALE'
dateOfBirth = date format YYYY-MM-DD
region - отправляй regionId
image - отправляется посредством multipart в отдельном поле (от запроса), в бинарном виде.
DESCRIPTION;
}
public static function validate(){
return [
'session' => ['type' => 'rpcUserSession' , 'validate' => [], 'req' => true ],
'nickname' => ['type' => 'string' , 'validate' => [], 'req' => false ],
'region' => ['type' => 'region' , 'validate' => [], 'req' => false ],
'gender' => ['type' => 'string' , 'validate' => [], 'req' => false ],
'dateOfBirth' => ['type' => 'string' , 'validate' => [], 'req' => false ],
'aboutUs' => ['type' => 'string' , 'validate' => [], 'req' => false ],
];
}
public function run(){
$RpcUser = $this->Data['session'];
$updateField = [];
if( $this->Data['nickname'] ){
// проверить зарегистрированность nickname
if ( \CustomRpc\EntityQuery\RpcUserQuery::checkRegisteredNickname( $this->Data['nickname'] ) ) {
$this->Response->setError('ERROR_NICKNAME_REGISTERED'); return;
}
$updateField['nickname'] = $this->Data['nickname'];
}
$this->Data['region'] && $updateField['region_id'] = $this->Data['region']->id;
$this->Data['gender'] && $updateField['gender'] = $this->Data['gender'];
$this->Data['dateOfBirth'] && $updateField['date_of_birth'] = $this->Data['dateOfBirth'];
$this->Data['aboutUs'] && $updateField['about_us'] = $this->Data['aboutUs'];
$file = \CustomRpc\Entity\RpcUserHelper::loadAndSaveImage('image');
$file && $updateField['image_id'] = $file->id();
\CustomRpc\EntityQuery\RpcUserQuery::update($RpcUser, $updateField);
$this->Response->setError('ERROR_NO');
}
public static function return(){
return [];
}
}
Весь процесс написания RPC метода сводиться к тому, что нам нужно написать класс (структуру в случае с go), описать схему валидации (можно описать структуру данных в отдельном классе или добавить свойства в текущий класс) и саму логику.
Были идеи, что RPC ядро должно запустить чистую логику, которая ничего не знает про api, но обычно, в этом не было большого смысла (это увеличивало количество кода, а переиспользования как такового не было)
По итогу мы получаем:
гибкость. Можно получать и отдавать запрос в разных форматах (на логику, это не влияет)
хорошую отказоустойчивость. Ошибки обрабатываются ядром RPC и вероятность падения сведена к миниму. В любом случае мы можем добавить дополнительную проверку/обработку, не меняя код методов.
простоту тестирования. Не нужно дергать сетевой слой. Есть возможность написать общие тесты для всех методов. Сам метод можно протестировать, передав сырые данные или же валидный объект.
документация RPC методов. В целом можно написать адаптер для swager'a или генерировать простенький html (я выбрал последнее).
Заключение
Я показывал пример RPC разным людям, кому то такой подход нравиться, кому то нет, кто то пытался сделать подобное. В любом случае есть куда расти и есть идеи для реализации. Примеры не тянут на оскар и демонстрируют как можно писать. RPC ядро на go можно посмотреть тут, на php тут (develop более свежее).