Генерация API сайта на основе заданных пользователем функций

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Основная идея

Идея достаточно простая: в пакете в определенной директории создаётся файл `.php` который должен возвращать анонимную функцию обработки следующего вида:

return function (ApiPut $api, string|int $id, array $value = []) {/**/};
return function (ApiGet $api, string|int $id) : array {/**/};
return function (ApiLifeTime $api) : array{/**/};
return function (ApiDirect $api, string|int $id) : array {/**/};

Именем функции служит имя файла + поддиректория. Т.е. для файла расположенного в `auth/user/get.php` будет сгенерировано имя `auth_user_get`.

Типы функций

В зависимости от первого параметра все функции подразделяются на четыре типа:

  • ApiPut - функция для изменения значений.

  • ApiGet - функция для чтения значений. Результат вызова кешируется до момента изменения зависимостей. К примеру функция возвращает текст статьи по идентификатору. При первом вызове происходит запрос в БД и кеширование результата. При последующих вызовах с тем же идентификатором возвращается кешированное значение. При вызове функции изменения статьи по этому идентификатору происходит сброс кешироемого значения.

  • ApiLifeTime - функция для чтения значений. Результат вызова кешируется до истечении заданного времени.

  • ApiDirect - функция прямого вызова. Для функций чтения (ApiGet и ApiLifeTime) игнорирует кешируемые значения и всегда вызывает заданную функцию.

Каждый тип объекта содержит функции, доступные в нем для вызова.

  • ApiPut - может вывать все функции, кроме функций вида ApiPut. Т.е. функция, изменяющая значение, не может вызвать другую изменяющую функцию. При этом все вызовы будут прямые, т.е. значения в кеше будут игнорироваться. Это связано с тем, что при изменении данных необходимы актуальные значения данных.

  • ApiGet - позволяет вызывать функции вида ApiGet. Функции вида ApiDirect недоступны, так как эти функции всегда возвращают разные значения, а значит их нельзя кешировать.

    Так как результат кеширования ApiLifeTime изменяется только от времени, то его изменение не приведет к пересчету кешироумого значения в функции вида ApiGet, поэтому вызовы функций вида ApiLifeTime также запрещены.

     Так как это функция чтения, то она не может ничего изменять, поэтому функции вида ApiPut в ней доступны, но только в режиме *зависимости*. Т.е. можно вызывать функцию вида ApiPut c указанием ключевых полей, что свяжет функцию ApiGet с функцией ApiPut через указанные параметры (но никакого измеенния данных не будет!). Это указание системе, что при вызове функции вида ApiPut с такими ключевыми параметрами необходимо сбрость закешированное в ApiGet значение. К примеру у нас функция получения статьи `article_get`:

 return function (ApiGet $api, string|int $id) : array {
    // Указать зависимость от функции article_put
    $api->article_put($id);
    // Выбрать статью из БД
    return db()->select('...')->get();
 };

Теперь если будет вызывана функция `article_put` для изменения статьи, то произойдет сброс закешированного значения.

  • ApiLifeTime - позволяет вызывать только функции вида ApiGet и ApiLifeTime.

 Итоговая схема вызовов:

Для генерации мы ищем все файлы с определенными функциями API и генерируем файл класса для вызова этих функций.

Данный файл не является конечным вариантом, а просто иллюстрирует что примерно должно получится.

class Api
{
    // Фукнции
    protected ApiPut $apiPut = new ApiPut;
    protected ApiGet $apiGet = new ApiGet;
    protected ApiLifeTime $apiLifeTime = new ApiLifeTime;
    protected ApiDirect $apiDirect = new ApiDirect;
    // Функция article_put
    protected $article_put_fn = null;
    public function article_put(string|int $id, array $data) {
        // Сгенерировать ключ КЕШ-а по ключу
        $key = 'article_get:id'.serialize([$id]);
        // Функция загружена?
        if( is_null($this->article_put_fn) ) {
            // Загрузить функцию
            $this->article_put_fn = require "<путь до файла функции>";
        }
        // Вызвать функцию
        $ret = $this->article_put_fn($this->generatorPut, $id,$data);
        // Сбросить закешированные зависимые значения
        cache()->remove($key);
        // Вернуть результат работы функции
        return $ret;
    }
    // Функция article_get
    protected $article_get_fn = null;
    public function article_get(string|int $id) {
        // Сгенерировать ключ КЕШ-а по ключу
        $key = 'article_get:id'.serialize([$id]);
        // Проверить наличие в КЕШ-е
        if( cache()->has($key) ) {
            // Если есть в КЕШ-е, то читать значение
            $ret = cache()->get($key);
        } 
        else 
        {
            // Функция загружена?
            if( is_null($this->article_get_fn) ) {
                // Загрузить функцию
                $this->article_get_fn = require "<путь до файла функции>";
            }
            // Вызвать функцию
            $ret = $this->article_get_fn($this->generatorGet, $id);
            // Записать значение в КЕШ навсегда
            cache()->put($key, $ret, 0);
        }
        // Вернуть результат работы функции
        return $ret;
    }
    // Функция article_lifetime
    protected $article_lifetime_fn = null;
    public function article_lifetime(string|int $id) {
        // Сгенерировать ключ КЕШ-а по ключу
        $key = 'article_get:id'.serialize([$id]);
        // Проверить наличие в КЕШ-е
        if( cache()->has($key) ) {
            // Если есть в КЕШ-е, то читать значение
            $ret = cache()->get($key);
        } 
        else 
        {
            // Функция загружена?
            if( is_null($this->article_get_fn) ) {
                // Загрузить функцию
                $this->article_get_fn = require "<путь до файла функции>";
            }
            // Вызвать функцию
            $ret = $this->article_get_fn($this->generatorLifeTime, $id);
            // Записать значение в КЕШ на заданное время
            cache()->put($key, $ret, $this->generatorLifeTime->getTTL());
        }
        // Вернуть результат работы функции
        return $ret;
    }
    // Функция article_direct
    protected $article_direct_fn = null;
    public function article_direct(string|int $id) {
        // Функция загружена?
        if( is_null($this->article_direct_fn) ) {
            // Загрузить функцию
            $this->article_direct_fn = require "<путь до файла функции>";
        }
        // Вызвать функцию
        return $this->article_direct_fn($this->generatorDirect , $id);
    }
}

Также генерируются файлы классов ApiPut, ApiGet, ApiLifeTime, ApiDirect. Так как у нас есть список всех функций и их параметров, то сгенерировать такие файлы - это дело техники.

При вызове из функции fn_a вида ApiGet функции fn_b вида ApiGet необходимо учитывать что функция fn_a зависит не только от своих связей, но от связей функции fn_b. Т.е. К примеру у нас вот такие зависимости:

В этом случае функция fn_b зависит от fn_z. А функция fn_a зависит от fn_z и fn_y (fn_b не учитываем, так как она не может измененять данные). Т.е. при вызове функции fn_z сбросится закешированное значение для функций fn_b и fn_a. А при вызове функции fn_y сбросится кешированное значение только функции fn_a.

Идея кеширования основана на докладе Уходим в кэш в высоконагруженных системах / Павел Паршиков (Авито)

ApiGet

У каждого ключа есть время установки. Каждый элемент КЕШ-а содержит

  • Значение

  • Все ключи от которых зависит значение и время установки этих ключей

При чтении данных из кеша происходит проверка всех зависимых ключей с текущим временем установки этих ключей. Если время какого-то ключа не совпадает,

значит значение в КЕШ-е нужно пересчитать.

При этом КЕШ хранения данных и КЕШ хранения временных меток ключей может быть разный. Т.к. к меткам времени доступ будет более частый, то имеет смысл хранить их в памяти. Тем более что ключ+метка времени будут занимать не так много памяти даже для большего количества ключей.

Рассмотрим более детально. К примеру у нас есть такая цепочка вызовов функций API:

Цепочка вызовов
Цепочка вызовов

При первом вызове в кеше данных устанавливаются следующие значения

Состояние КЕШ-а данных
Состояние КЕШ-а данных

Вместе с данными сохраняется время установки значения и время установки всех дочерних вызовов. Во всех случаях она = **t1** (хотя на практике значения могут отличаться, но в нашем примере предположим что все временные метки имеют одинаковое значение).

Также имеется кеш временных меток.

Состояние КЕШ-а временных меток
Состояние КЕШ-а временных меток

Состояние 1 показывает значения кеша временных меток после первого вызова. При вызове функции установки значения  fn_z происходит сброс временной метки для функции fn_c.

Состояние 2 показывает значения кеша временных меток после сброса КЕШ-а функции fn_c.

При запросе значения происходит:

1. Запрос данных из КЕШ-а данных

2. Проверка что временные метки всех дочерних ключей соответствуют тому что сохранены в КЕШ-е данных в поле rel_keys.

Если временная метка отличается (или отсутствует), то выполняется повторная генерация значения. Если все метки совпали, то значит данные имеют актуальное значение.

ApiLeftTime

Храним значение и время до которого это значение валидно. При чтении происходит проверка времени. Если время превышено, то

  1. Изменяем время на +30 секунды (значение не важно, главное чтобы оно было больше чем генерируются новые данные)

  2. Запускаем функцию генерации новых данных

  3. После генерации новых данных изменяем значение в КЕШ-е

Это позволит уменьшить нагрузку на сервер в случае пересчета данных. Т.е. только первый запрос вызовет их пересчет, остальные будут читать либо уже "продленные", либо новые данные.

Пример вызова API функций

Все API функции вызываются из соответствующего сервиса

    // Запуск
    public function run(IApi $api)
    {
        // Вызов функции типа ApiPut
        $api->test_dbg_put(1, ['aa' => 11]);
        // Вызов функции типа ApiGet
        $ret1 = $api->test_dbg_get(1);
        // Вызов функции типа ApiDirect
        $ret2 = $api->test_dbg_direct();
    }

Итоги

Разработчику не нужно думать о кешировании, достаточно просто написать функцию и указат ьеё тип. Всё остальное будет сгенерировано автоматически.

Единственный сервис который необходим для получения данных - это сервис сгенерированного API.

В качестве бонуса можно генерировать код для вызова API на frontend-е.

В этом случае одну и туже функцию, к примеру, для  получения статьи можно использовать как на backend-е, так и на frontend-е. Но тут ещё нужно добавить условие, что все данные, что возвращает функция API должна конвертироваться в формат JSON.

Источник: https://habr.com/ru/post/700926/


Интересные статьи

Интересные статьи

Эта статья рассказывает о моём опыте создания пользовательских представлений под Android. Со временем это всё переросло в целую библиотеку.Как-то одним вечером мне в голову забралась идея о создании с...
Привет! Меня зовут Саша Шутай, я тимлид в AGIMA. В прошлой статье я рассказывал, что делать, если на проекте Bitrix сожительствует с Vue.js и поисковые боты не видят контента сайта. А в этой помогу ра...
Привет! Меня зовут Максим Бондарев, я работаю младшим разработчиком в компании Digital Design и заканчиваю обучение на математико-механическом факультете СПбГУ. В рамках своей исследовательской работы...
Появившиеся в 2006 году сервисы Google по работе с текстовыми документами (Google Docs) и таблицами (Google Sheets), дополненные 6 лет спустя возможностями работы с вирту...
Всем привет! Приведенный ниже материал является продолжением статьи о функционале, добавленном в новой версии MQTTv5.0. Если вы уже успели ее изучить, то для вас не составит труда...