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

Актуально для версии 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