Child pages
  • Туториал по написанию API для модуля
Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 24 Current »

Данная страница документация не завершена.

Актуально для версии 1.22.00.

Все действия будут производиться в папке модуля, например, Abills/modules/Portal.
За пример взят модуль Portal.

Старт

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

Начало API модуля
# Мы объявляем 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

Он должен выглядеть приблизительно так:

словарь ошибок Portal::Errors
# Пэкэдж должен ОБЯЗАТЕЛЬНО иметь название *модуль*::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 с таким примерным содержанием:

схемы валидатора Portal::Validations
# Называем пэкэдж *модуль*::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:

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

CRUD роуты
  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

Со следующим содержанием:

контроллер Portal::Api::admin::Attachment
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:

schemas/articlesList.yaml
type: object
properties:
  list:
    type: array
    items:
      $ref: "./article.yaml"
  total:
    type: number
    example: 173

Поскольку это стандартный список в системе, то лучше item отдельно делить в схему.
Это позволит переиспользовать его в пути с одиничным определением /portal/articles/{ID}

schemas/article.yaml
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, заполняем и это:

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 совсем ничем не отличается.

контроллер Portal::Api::user::News
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/paths.yaml
/user/portal/news:
  $ref: "./paths/news.yaml"

И в ./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:

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 - лимит с таблицы

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

  • No labels