Актуально для версии 1.22.00.
Все действия будут производиться в папке модуля, например, Abills/modules/Portal.
За пример взят модуль Portal.
Старт
Для начала, в корневой папке модуля файл Api.pm, с таким начальным содержанием:
# Мы объявляем package с названием "*модуль*::Api".
package Portal::Api;
=head1 NAME
Portal Api
=cut
use strict;
use warnings FATAL => 'all';
# Импортируем сообщения для ошибок
use Control::Errors;
my Control::Errors $Errors;
#**********************************************************
=head2 new($db, $conf, $admin, $lang, $debug, $type)
=cut
#**********************************************************
# Создаём конструктор
sub new {
my ($class, $db, $admin, $conf, $lang, $debug, $type) = @_;
my $self = {
db => $db,
admin => $admin,
conf => $conf,
lang => $lang,
debug => $debug
};
bless($self, $class);
$self->{routes_list} = ();
# Определяем, для чего роутер вызвал наш модуль API
# Соответственно, ли это USER API или ADMIN API
# И записываем routes_list
if ($type eq 'user') {
$self->{routes_list} = $self->user_routes();
}
elsif ($type eq 'admin') {
$self->{routes_list} = $self->admin_routes();
}
$Errors = Control::Errors->new($self->{db}, $self->{admin}, $self->{conf},
# Обязательно обозначить что это за модуль,
# чтобы система могла подгрузить сообщения для ошибок со словаря модуля за потребности
{ lang => $lang, module => 'Portal' }
);
# Сохраняем словарь ошибок в объект, он нам потом будет нужен
$self->{Errors} = $Errors;
return $self;
}
#**********************************************************
=head2 admin_routes() - Returns available ADMIN API paths
=cut
#**********************************************************
sub admin_routes {
# Здесь нужно возвращать специальный массив ADMIN API
return [];
}
#**********************************************************
=head2 user_routes() - Returns available USER API paths
=cut
#**********************************************************
sub user_routes {
# Здесь нужно возвращать специальный массив USER API
return [];
}
1;
Словарь ошибок
Документация по словарю ошибок
Важная часть - позволяет систематизировать ошибки и избежать разночтений ошибок во время написания кода.
Создаем в корневой папке модуля файл Errors.pm
Он должен выглядеть приблизительно так:
# Пэкэдж должен ОБЯЗАТЕЛЬНО иметь название *модуль*::Errors
# Это позволяет библиотеке словаря ошибок его же найти
package Portal::Errors;
=head1 NAME
Portal::Errors
# Крайне рекомендуем указывать в pod какой префикс ошибок у модуля.
# Это просто поможет в поддержке, понимать какой префикс у какого модуля.
IDS: 144*
=cut
use strict;
use warnings FATAL => 'all';
#**********************************************************
=head2 errors() - errors list
=cut
#**********************************************************
# Создаём функцию словаря ошибок с анонимным хэшэм.
# В будущем он будет заполнен парами *errno* => *errstr*
sub errors {
return {
# Семизначный код ошибки, с приставкой 144 (модуль Portal) => ключ в словаре ошибки
# Примеры:
1440001 => 'ERR_PORTAL_NO_SENDER',
1440002 => 'ERR_PORTAL_NO_ARTICLE',
};
}
1;
И не забудьте заполнить lng_english.pl
# Заполняем ключи ошибок, можем даже вставлять переменные
$lang{ERR_PORTAL_NO_SENDER} = 'No sender with id %ID%';
$lang{ERR_PORTAL_NO_ARTICLE} = 'No article with id %ID%';
Валидатор
Документация по валидатору
Позволяет проверять полученный реквест от клиента по схеме.
Избегает вызова пути с неправильными параметрами, что повышает устойчивость системы и возможных ошибок.
Также позволяет чётко и подробно расписывать какие именно параметры отсутствуют или неправильные в целом.
Создаём корневой файл Validations.pm с таким примерным содержанием:
# Называем пэкэдж *модуль*::Validations
package Portal::Validations;
use strict;
use warnings FATAL => 'all';
# Вызываем специальный модуль экспортера, для удобного экспортирования констант с описанием в IDE
use Exporter;
use parent 'Exporter';
# Записываем константы валидации в экспорт
our @EXPORT = qw(
POST_PORTAL_ARTICLES
);
# Записываем константы валидации в экспорт
our @EXPORT_OK = qw(
POST_PORTAL_ARTICLES
);
use constant {
# Называем константу *МЕТОД*_ПУТЬ_
# А подробнее про валидатор можно узнать ссылкой выше.
POST_PORTAL_ARTICLES => {
TITLE => {
required => 1,
type => 'string',
min_length => 5,
max_length => 255
},
DATE => {
required => 1,
type => 'date'
},
PORTAL_MENU_ID => {
required => 1,
type => 'string'
},
SHORT_DESCRIPTION => {
type => 'string',
max_length => 600
},
CONTENT => {
type => 'string',
}
},
};
1;
ADMIN API
Создание роутов
Мы создали обязательное начало для API, теперь, если вам это нужно, создаём роуты для ADMIN API:
#**********************************************************
=head2 admin_routes() - Returns available ADMIN API paths
=cut
#**********************************************************
sub admin_routes {
my $self = shift;
return [
{
# HTTP метод, GET, POST, PUT, PATCH, DELETE
method => 'GET',
# Абсолютный путь, за которым можно будет достучаться, например billing.url/api.cgi/portal/articles
path => '/portal/articles/',
# Указываем "контроллер" для API /portal/articles/*
controller => 'Portal::Api::admin::Articles',
# Даём ссылку на функцию-эндпойнт контроллера
endpoint => \&Portal::Api::admin::Articles::get_portal_articles,
credentials => [
# Определяем нужные параметры для авторизации.
# ADMIN - API_KEY
# ADMINSID - admin_sid по cookie (в том числе для api_call)
'ADMIN', 'ADMINSID'
]
},
]
}
Мы создали свой первый роут, но нам ещё нужно создать для его base первый контроллер.
Вы, конечно, можете писать функцию сразу же в этом хэндлере, но мы не рекомендуем так делать, поскольку в будущем вам станет неудобно это поддерживать.
Поскольку практически каждая функция администратора должна иметь CRUD-составляющую, то вот как это выглядит для /portal/articles
return [
# Создать статью
{
method => 'POST',
path => '/portal/articles/',
# Параметры до валидатора
params => POST_PORTAL_ARTICLES,
controller => 'Portal::Api::admin::Articles',
endpoint => \&Portal::Api::admin::Articles::post_portal_articles,
credentials => [
'ADMIN', 'ADMINSID'
]
},
# Получить статьи
{
method => 'GET',
path => '/portal/articles/',
controller => 'Portal::Api::admin::Articles',
endpoint => \&Portal::Api::admin::Articles::get_portal_articles,
credentials => [
'ADMIN', 'ADMINSID'
]
},
# Получить конкретную статью
{
method => 'GET',
path => '/portal/articles/:id/',
controller => 'Portal::Api::admin::Articles',
endpoint => \&Portal::Api::admin::Articles::get_portal_articles_id,
credentials => [
'ADMIN', 'ADMINSID'
]
},
# Изменить конкретную статью
{
method => 'PUT',
path => '/portal/articles/:id/',
controller => 'Portal::Api::admin::Articles',
endpoint => \&Portal::Api::admin::Articles::put_portal_articles_id,
credentials => [
'ADMIN', 'ADMINSID'
]
},
# Удалить конкретную статью
{
method => 'DELETE',
path => '/portal/articles/:id/',
controller => 'Portal::Api::admin::Articles',
endpoint => \&Portal::Api::admin::Articles::delete_portal_articles_id,
credentials => [
'ADMIN', 'ADMINSID'
]
},
]
Создание контроллера
Соответственно, как наши пути будут в /portal/articles/* и всё что с этим связано, и мы находимся в ADMIN API, то рекомендуем создать файл за такой схемой:
Api/*тип API*/*Контроллер*.pm
например Api/admin/Articles.pm
Со следующим содержанием:
package Portal::Api::admin::Articles;
=head1 NAME
Portal articles manage
# Рекомендуем в подах записывать к каким
# группам эндпойнтов относится данный контроллер
Endpoints:
/portal/articles/*
=cut
use strict;
use warnings FATAL => 'all';
use Control::Errors;
# Импортируем объект Portal для работы с базой
# он должен находиться в /usr/abills/Abills/mysql/Portal.pm
use Portal;
my Control::Errors $Errors;
my Portal $Portal;
my Portal::Misc::Attachments $Attachments;
#**********************************************************
=head2 new($db, $admin, $conf)
=cut
#**********************************************************
sub new {
my ($class, $db, $admin, $conf, $attr) = @_;
my $self = {
db => $db,
admin => $admin,
conf => $conf,
attr => $attr
};
bless($self, $class);
$Portal = Portal->new($db, $admin, $conf);
$Attachments = Portal::Misc::Attachments->new($self->{db}, $self->{admin}, $self->{conf});
# Определяем словарь ошибок, который нам пришёл выше
$Errors = $self->{attr}->{Errors};
return $self;
}
# определяем здесь пути
1;
Определение роутов
Именно здесь вы можете определять базовую бизнес-логику.
Поскольку мы пытаемся в CRUD - определяем.
Create
#**********************************************************
=head2 post_portal_articles($path_params, $query_params)
Endpoint POST /portal/articles
=cut
#**********************************************************
sub post_portal_articles {
my $self = shift;
my ($path_params, $query_params) = @_;
if ($query_params->{PICTURE}) {
my $picture_name = $Attachments->save_picture($query_params->{PICTURE});
$query_params->{PICTURE} = $picture_name;
}
my $permalink = $query_params->{PERMALINK} || _portal_generate_permalink($query_params->{TITLE});
return $Portal->portal_article_add({ %$query_params, PERMALINK => $permalink });;
}
Read
#**********************************************************
=head2 get_portal_articles($path_params, $query_params)
# Всегда пишите в подах Endpoint *METHOD* *path*
# Это позволит легче искать путь во время разработки.
Endpoint GET /portal/articles
=cut
#**********************************************************
sub get_portal_articles {
my $self = shift;
my ($path_params, $query_params) = @_;
# Определяем системные параметры, сортировки, пагинации
my %PARAMS = (
COLS_NAME => 1,
PAGE_ROWS => $query_params->{PAGE_ROWS} ? $query_params->{PAGE_ROWS} : 25,
SORT => $query_params->{SORT} ? $query_params->{SORT} : 1,
PG => $query_params->{PG} ? $query_params->{PG} : 0,
DESC => $query_params->{DESC},
);
# Даём возможность сортировки с помощью ?filename&file_size&file_type
foreach my $param (keys %{$query_params}) {
$query_params->{$param} = ($query_params->{$param} || "$query_params->{$param}" eq '0')
? $query_params->{$param}
: '_SHOW';
}
# Вызываем функцию для извлечения списка из базы, с нашими параметрами и которые определены вызовом
# которые будут внутри обрабатываться search_former
my $list = $Portal->portal_articles_list({ %$query_params, %PARAMS });
my @result = map {
my $article_sublink = $_->{permalink} || $_->{id};
my $picture_link = $_->{picture} ? $self->_portal_picture_link($_->{picture}) : '';
$_->{url} = $self->_portal_news_link($article_sublink);
$_->{picture} = $picture_link;
$_
} @$list;
# Настоятельная рекомендация: когда вы создаёте путь, который возвращает массив, то возвращайте
# его с объектом с ключём list, а в total возвращайте общее число айтемов - это позволит работать пагинации
# и опеределять дополнительные параметры. Будет легче поддерживать.
return {
list => \@result,
total => $Portal->{TOTAL}
};
}
#**********************************************************
=head2 get_portal_articles_id($path_params, $query_params)
Endpoint GET /portal/articles/:id/
=cut
#**********************************************************
sub get_portal_articles_id {
my $self = shift;
my ($path_params, $query_params) = @_;
foreach my $param (keys %{$query_params}) {
$query_params->{$param} = ($query_params->{$param} || "$query_params->{$param}" eq '0')
? $query_params->{$param}
: '_SHOW';
}
$query_params->{COLS_NAME} = 1;
my $list = $Portal->portal_articles_list({ ID => $path_params->{id}, %$query_params });
my @result = map {
my $article_sublink = $_->{permalink} || $_->{id};
my $picture_link = $_->{picture} ? $self->_portal_picture_link($_->{picture}) : '';
$_->{url} = $self->_portal_news_link($article_sublink);
$_->{picture} = $picture_link;
$_
} @$list;
return $result[0] || {};
}
Update
#**********************************************************
=head2 put_portal_articles_id($path_params, $query_params)
PUT /portal/articles/:id
=cut
#**********************************************************
sub put_portal_articles_id {
my $self = shift;
my ($path_params, $query_params) = @_;
if ($query_params->{PICTURE}) {
my $picture_name = $Attachments->save_picture($query_params->{PICTURE}, $path_params->{id});
$query_params->{PICTURE} = $picture_name;
}
my $permalink = $query_params->{PERMALINK} || _portal_generate_permalink($query_params->{TITLE});
return $Portal->portal_article_change({ %$query_params, PERMALINK => $permalink });
}
Delete
#**********************************************************
=head2 delete_portal_articles_id($path_params, $query_params)
Endpoint DELETE /portal/articles/:id/
=cut
#**********************************************************
sub delete_portal_articles_id {
my $self = shift;
my ($path_params, $query_params) = @_;
my $list = $Portal->portal_articles_list({ ID => $path_params->{id}, COLS_NAME => 1 });
if (!($list && scalar(@$list))) {
# Не забудьте написать этот код ошибки в словаре!
return $Errors->throw_error(1440002, { lang_vars => { ID => $path_params->{id} }});
}
my $result = $Portal->portal_article_del({ ID => $path_params->{id} });
if (!$Portal->{errno}) {
$Attachments->delete_attachment($path_params->{id});
}
return $result;
}
Написание OpenAPI
Про OpenAPI
Обязательная составляющая, так как нужно разработчикам узнать, как с вашим API взаимодействовать.
У нас есть микрофреймворк с работой "сверху" над ним, чтобы учитывать нашу модульность.
Помним, что в рамках определения OpenAPI модуля есть:
- Файл Api/swagger/(admin|user)/paths.yaml - основа
- Папка Api/swagger/(admin|user)/paths - определения путей
- Папка Api/swagger/(admin|user)/schemas - определения схем
Создаём файл Abills/modules/Portal/Api/swagger/admin/paths.yaml
И определяем:
/portal/articles:
$ref: "./paths/articles.yaml"
/portal/articles/{ID}:
$ref: "./paths/article.yaml"
Мы записываем базисы путей, и где они определяются:
get:
tags:
- portal
summary: Список статей
parameters:
- name: pageRows
in: query
description: Количество записей
schema:
type: integer
default: 25
- name: sort
in: query
description: Сортировка по одному параметру выше
schema:
type: string
- name: pg
in: query
description: Работает вместе с pageRows, параметр отвечает с какой записи начинать возвращать в запросе
schema:
type: integer
default: 0
responses:
200:
description: Успешное выполнение
content:
application/json:
schema:
$ref: "../schemas/articlesList.yaml"
security:
- KEY: [ ]
post:
tags:
- portal
summary: Добавление статьи
operationId: addArticlePortal
requestBody:
description: Параметры, которые нужно указать
content:
application/json:
schema:
$ref: "../schemas/articleAddRequest.yaml"
required: true
responses:
200:
description: успешное выполнение
content:
application/json:
schema:
$ref: "../schemas/articleAddResponse.yaml"
security:
- KEY: [ ]
К стандарту OpenAPI, обязательно изучите другие параметры.
Повсюду, где можно определить schema - стараемся делить в ../schemas/*.
Пример articlesList:
type: object
properties:
list:
type: array
items:
$ref: "./article.yaml"
total:
type: number
example: 173
Поскольку это стандартный список в системе, то лучше item отдельно делить в схему.
Это позволит переиспользовать его в пути с одиничным определением /portal/articles/{ID}
type: object
properties:
id:
type: integer
example: 164
archive:
type: integer
example: 0
addressFlat:
type: string
example: "4"
buildId:
type: number
example: 8
content:
type: string
example: "<p>Новая Open Source версия биллинга!</p>\n\n\n\n<p>Полный список новинок, исправлений и улучшений биллинга к новому релизу!</p>"
date:
type: string
example: "2023-01-27"
deeplink:
type: integer
example: 1
districtId:
type: string
example: "8"
domainId:
type: integer
example: 0
endDate:
type: string
example: ""
etimestamp:
type: integer
example: 0
gid:
type: integer
example: 0
importance:
type: integer
example: 0
name:
type: string
example: "Releases"
onMainPage:
type: integer
example: 0
permalink:
type: string
example: "releases-abills-095-blackout"
picture:
type: string
example: "https://demo.abills.net.ua:9443/images/attach/portal/13863233.jpg"
portalMenuId:
type: integer
example: 8
shortDescription:
type: string
example: "Встречайте новый релиз 2023"
stName:
type: string
example: ""
status:
type: integer
example: 1
streetId:
type: integer
example: 0
tagName:
type: string
example: ""
tags:
type: integer
example: 0
title:
type: string
example: "Релиз ABillS 0.95 Blackout"
url:
type: string
example: "https://demo.abills.net.ua:9443/?article=release-abills-095-blackout"
utimestamp:
type: integer
example: 1674815301
И в конце запускаем misc/api/generate_docs.pl, и проверяем bundle_admin.yaml.
И поздравляем, вы полностью разработали и описали ADMIN API для модуля.
USER API
Создание роутов
Если нужно USER API, заполняем и это:
#**********************************************************
=head2 user_routes() - Returns available USER API paths
=cut
#**********************************************************
sub user_routes {
my $self = shift;
return [
{
method => 'GET',
# Для USER API ОБЯЗАТЕЛЬНО начинаем абсолютный путь с /user/*.
path => '/user/portal/menu/',
# Подключаем "контроллер" для API /user/portal/*
controller => 'Portal::Api::user::News',
# Даём ссылку на функцию-эндпойнт контроллера
endpoint => \&Portal::Api::user::News::get_user_portal_news,
credentials => [
# Определяем нужные параметры для авторизации.
# USER - авторизация по header, полученном с /user/login
# USERSID - авторизация по cookie (в том числе для api_call)
# PUBLIC - без авторизации
# Тоесть, в данном случае путь может работать как и с авторизованными пользователями, так и нет.
# Внутри хэндлера можно определять какой пользователь, об этом ниже.
'USER', 'USERSID', 'PUBLIC'
]
},
]
}
Создание контроллера
Контроллер для USER API совсем ничем не отличается.
package Portal::Api::user::News;
=head1 NAME
User Portal
# Рекомендуем в подах записывать к каким
# группам эндпойнтов относится данный контроллер
Endpoints:
/user/portal/news*
/user/portal/menu
=cut
use strict;
use warnings FATAL => 'all';
use Control::Errors;
# Импортируем объект Portal для работы с базой
# он должен находиться в /usr/abills/Abills/mysql/Portal.pm
use Portal;
my Portal $Portal;
my Control::Errors $Errors;
#**********************************************************
=head2 new($db, $admin, $conf)
=cut
#**********************************************************
sub new {
my ($class, $db, $admin, $conf, $attr) = @_;
my $self = {
db => $db,
admin => $admin,
conf => $conf,
attr => $attr
};
bless($self, $class);
$Portal = Portal->new($db, $admin, $conf);
# Определяем словарь ошибок, который нам пришёл выше
$Errors = $self->{attr}->{Errors};
return $self;
}
# здесь определять пути
1;
Определение роутов
В целом, определение роутов для USER API ничем не отличается от ADMIN API, кроме одной важной детали - в $path_params при авторизации будет приходить uid.
Очень важная составляющая.
#**********************************************************
=head2 get_user_portal_news($path_params, $query_params)
Endpoint GET /user/portal/news
=cut
#**********************************************************
sub get_user_portal_news {
my $self = shift;
my ($path_params, $query_params) = @_;
# Не обязательно писать всю логику прямо внутри эндпоинта, как в примере с ADMIN API
# Вы можете делить логику в бизнес-функции, для сокращения использования.
# Но для простоты понимания, с самого начала лучше писать всё в эндпоинтах
return $self->_portal_menu({
# Если пользователь авторизован - в $path_params->{uid} будет UID пользователя.
# Если нет - поле будет пустое.
UID => $path_params->{uid} || '',
DOMAIN_ID => $query_params->{DOMAIN_ID},
PORTAL_MENU_ID => $query_params->{PORTAL_MENU_ID},
MAIN_PAGE => $query_params->{MAIN_PAGE},
LIST => 1
});
}
Написание OpenAPI
Каждое API нужно описывать.
Поэтому, по нашему микрофреймворку над OpenAPI
- Файл Api/swagger/(admin|user)/paths.yaml - основа
- Папка Api/swagger/(admin|user)/paths - определения путей
- Папка Api/swagger/(admin|user)/schemas - определения схем
Файл user/paths.yaml:
/user/portal/news:
$ref: "./paths/news.yaml"
И в ./paths/news.yaml:
get:
tags:
- portal
summary: Список новостей
responses:
200:
description: Успешное выполнение
content:
application/json:
schema:
$ref: "../schemas/news.yaml"
security:
- USERSID: [ ]
schemas/news.yaml:
type: object
properties:
news:
type: array
items:
type: object
properties:
content:
type: string
example: При замовленні пакету «ABillS+» платіть X грн/міс замість X грн/міс. Підключайте ТВ-пакет «База+» і отримаєте в подарунок півроку користування послугою! Кожен місяць сплачуйте лише 50% вартості пакету. Абонплата складе всього X грн.
date:
type: string
format: date
example: 2023-01-23
id:
type: number
example: 4
importance:
type: number
example: 1
onMainPage:
type: number
example: 1
permalink:
type: string
example: special-promotion-50-2023
picture:
type: string
example: "http://192.168.99.2:9443/images/attach/portal/9700077.png"
shortDescription:
type: string
example: Тарифний план для всіх у сімї
title:
type: string
example: Акціний тариф візьми поки можеш
topicId:
type: number
example: 1
topics:
type: array
items:
type: object
properties:
id:
type: number
example: 1
name:
type: string
example: Акції від нас
url:
type: string
example: "https://demo.abills.net.ua:9443/"
И в конце запускаем misc/api/generate_docs.pl, и проверяем bundle_user.yaml.
И поздравляем, вы полностью разработали и описали USER API для модуля.
Практики
Поиск, сортировка, пагинация
Для правильной работы сортировки мы рекомендуем делать это не вручну, а с помощью search_former которые находятся внутри любого современного модуля.
Он выглядит приблизительно так, с точки зрения пути GET /portal/menus
#**********************************************************
=head2 get_portal_menus($path_params, $query_params)
Endpoint GET /portal/menus
=cut
#**********************************************************
sub get_portal_menus {
my $self = shift;
my ($path_params, $query_params) = @_;
# Проверяем, определяем стандартные параметры для вызова
my %PARAMS = (
COLS_NAME => 1,
PAGE_ROWS => $query_params->{PAGE_ROWS} ? $query_params->{PAGE_ROWS} : 25,
SORT => $query_params->{SORT} ? $query_params->{SORT} : 1,
PG => $query_params->{PG} ? $query_params->{PG} : 0,
DESC => $query_params->{DESC},
);
foreach my $param (keys %{$query_params}) {
$query_params->{$param} = ($query_params->{$param} || "$query_params->{$param}" eq '0')
? $query_params->{$param}
: '_SHOW';
}
my $list = $Portal->portal_menu_list({
ID => '_SHOW',
NAME => '_SHOW',
URL => '_SHOW',
DATE => '_SHOW',
STATUS => '_SHOW',
# С помощью внутренней деструктуризации присваиваем
%$query_params,
# Оверрайдим параметры
%PARAMS,
});
return {
list => $list,
total => $Portal->{TOTAL}
};
}
И смотрим что происходит под капотом у portal_menu_list
#**********************************************************
=head2 function portal_menu_list() - get menu section list
Arguments:
$attr
Returns:
\@list -
Examples:
my $list = $Portal->portal_menu_list({COLS_NAME=>1});
=cut
#**********************************************************
sub portal_menu_list {
my $self = shift;
my ($attr) = @_;
# Преопределяем параметры, если их нет
my $SORT = ($attr->{SORT}) ? $attr->{SORT} : 1;
my $DESC = ($attr->{DESC}) ? $attr->{DESC} : '';
my $PG = $attr->{PG} ? $attr->{PG} : 0;
my $PAGE_ROWS = $attr->{PAGE_ROWS} ? $attr->{PAGE_ROWS} : 25;
# Эта функция на основе пришедших параметров и паттерном формирует $WHERE clause.
my $WHERE = $self->search_former($attr, [
[ 'ID', 'INT', 'pm.id', 1 ],
[ 'NAME', 'STR', 'pm.name', 1 ],
[ 'URL', 'STR', 'pm.url', 1 ],
[ 'DATE', 'STR', 'DATE(pm.date) as date', 1 ],
[ 'STATUS', 'INT', 'pm.status', 1 ],
], { WHERE => 1 });
$self->query(
"SELECT $self->{SEARCH_FIELDS} pm.id
FROM portal_menu pm
$WHERE
ORDER BY $SORT $DESC LIMIT $PG, $PAGE_ROWS;;",
undef, $attr
);
my $list = $self->{list} || [];
# Берём общий count
$self->query("SELECT COUNT(*) AS total FROM portal_menu pm
$WHERE;",
undef,
{ INFO => 1 }
);
return $list || [];
}
И вот как раз вы можете в $query_params при вызове делать условный /portal/menu/?url=call*&name=test, и оно соответственно создаст выражения для поиска.
Полностью автоматически!
А насчёт сортирования, есть специальные параметры:
SORT - параметр для сортировки
DESC - по возрастанию или убыванию
А пагинации и лимита:
PG - какая страница
PAGE_ROWS - лимит с таблицы
Это позволяет формировать единые экспрессии по всей системе, без ручного определения.