Пошаговое осваивание фреймворка ABillS

Писалось для версии 0.77.14

В данном руководстве, Вы пошагово ознакомитесь с основными частями системы.
Фреймворк состоит из нескольких частей:

  • Ядро вебинтерфейса (движок) (index.cgi)
  • Работа с БД (dbcore.pm)
  • Работа с визуализацией
  • Базовые библиотеки
  • Файл конфигурации

Сначала рассмотрим составные части модуля, чтобы понимать, что нужно для написания логически интегрированного функционала.
В ABillS основная часть кода написана в функциональном или процедурном стиле, что влияет на работу с системой. Кроме того, поскольку ООП не используется для полиморфизма или расширения функционала классов через наследование, многие функции принимают аргумент $attr, в котором записаны дополнительные условия выполнения (которые могут кардинально изменять как результат так и логику выполнения), поэтому нужно всегда учитывать полную сигнатуру вызова при чтении кода.

Структура модуля ABillS

Модуль ABillS (для примера Example) состоит из:

  • Класса менеджера сущностей в БД. (Abills/mysql/Example.pm)
  • Файл с логикой (функциями) Abills/modules/Example/webinterface
  • Описание меню Abills/modules/Example/config
  • Дополнительные файлы словарных переменных (необязательно) (Abills/modules/lng_english.pl)
  • Файла описания схемы БД (db/Example.sql)

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

Регистрация модуля в движке

После добавления имени модуля в массив @MODULES (libexec/config.pl), при инициализации движка читается файл config из папки модуля и словарь с переменными текущего языка пользователя вебинтерфейса. Функции из config добавляются в глобальные реестр функций интерфейса, при этом каждой присваивается особый числовой индекс, который позволяет вызвать эту функцию.

Сами функции должны быть доступны в webinterface (или быть импортированы из других пакетов внутри webinterface)
Поскольку все webinterface выполняются в глобальной области видимости, к имени каждой функции нужно добавлять имя модуля.

Основная часть модуля - вебинтерфейс

В основном случае, логика вебинтерфейса проста и прозрачна - получить данные, обработать и вывести в каком-то виде (шаблон или таблица).
Фреймворк неявно (через глобальную область видимости) передаёт в webinterface следующие переменные:

Имя Описание
%LANG хеш-масив словаря
%FORM хеш-масив значений переданных на страницу (GET или POST запросом)
$html Обьект визуализации (экземпляр класса Abills::HTML)
$users Менеджер работы с пользователями (экземпляр класса Users)
$db Соединение с БД
$admin Менеджер работы с администраторами (экземпляр класса Admins)
%conf хеш-масив конфигурационного файла

Для примера рассмотрим работу с сущностью entity в модуле Example

Получить данные можно несколькими способами:

  • Из БД (ссылка на работу с БД)
  • Из внешнего источника (здесь ссылка на web_request)
  • Из файловой системы
  • Из других модулей

Для CRUD операций в ABillS принято использовать одну отдельную функцию в которой происходят следующие операции:

  • Добавление новой сущности
  • Редактирование сущности
  • Удаление сущности
  • Отображение списка сущностей
  • Отображение сущности (совмещено с редактированием)

Если используется работа с БД, то внутри файла webinterface инициализируется обьект менеджера работы с сущностями.

use Example; # Загрузить файл /usr/abills/Abills/mysql/Example.pm
my $Example = Example->new($db, $admin, \%conf); # Создать объект менеджера

В коде функция работы с сущностями будет выглядеть так:

#**********************************************************
=head2 entity_example_main()
 
=cut
#**********************************************************
sub entity_example_main{
  # Хеш для переменных шаблона обьявляется в области видимости функции
  my %template_args = ();
 
  # Флаг отображения шаблона
  my $show_template = $FORM{add_form} || 0;
 
  # Здесь используется глобальный хеш %FORM, 
  # который доступен в глобальной области видимости
  # и включает значения, полученные из GET или POST запроса.
  if ($FORM{add}) {
    $Example->entity_add({%FORM});
    $show_template = !show_result($Example, $lang{ADDED});
  }
  elsif ($FORM{change}) {
    $Example->entity_change({%FORM});
    show_result($Example, $lang{CHANGED});
    $show_template = 1;
  }
  elsif ($FORM{chg}) {
    my $entity_info = $Example->entity_info($FORM{chg});
    if (!_error_show($Entity)) {
      %template_args = %{$entity_info};
      $show_template = 1;
    }
  }
  elsif ($FORM{del} && $FORM{COMMENTS}) {
    $Example->entity_del({ ID => $FORM{del}, COMMENTS => $FORM{COMMENTS} });
    show_result($Example, $lang{DELETED});
  }
 
  # Использование этой точки выхода
  # позволяет использовать эту же функцию
  # только для выполнения операции (например AJAX запросом)
  return 1 if $FORM{MESSAGE_ONLY};
 
  # Здесь собрана логика обработки данных для отображения шаблона  
  if ($show_template) {
    # Отображение шаблона
    $html->tpl_show(
      _include('example_entity', 'Example'),
      {
        %TEMPLATE_ARGS,
        %FORM,
        SUBMIT_BTN_ACTION => ($FORM{chg}) ? 'change' : 'add',
        SUBMIT_BTN_NAME   => ($FORM{chg}) ? $lang{CHANGE} : $lang{ADD},
      }
    );
  }
 
  # Использование этой точки выхода
  # позволяет использовать эту же функцию
  # для отображения шаблона изменения внутри модального окна
  return 1 if ($FORM{TEMPLATE_ONLY});
 
  # Использование библиотеки Abills::ResultFormer
  # для получения списка из БД (метод $Example->entities_list($attr)) и построения таблицы (Abills::HTML->table($attr))
  my Abills::HTML $table; ($table) = result_former(
    {
      INPUT_DATA      => $Example,
      FUNCTION        => 'entities_list',
      BASE_FIELDS     => 0,
      DEFAULT_FIELDS  => 'ID,NAME,VALUE',
      FUNCTION_FIELDS => 'change,del',
      SKIP_USER_TITLE => 1,
      EXT_FIELDS      => 0,
      EXT_TITLES      => {
        id             => '#',
        name           => $lang{NAME},
        value          => $lang{VALUE},
      },
      TABLE => {
        width   => '100%',
        caption => $lang{ENTITY},
        ID      => 'ENTITIES_TABLE',
        EXPORT  => 1,
        MENU    => "$lang{ADD}:index=$index&add_form=1:add"
      },
      MAKE_ROWS     => 1,
      SEARCH_FORMER => 1,
      MODULE        => 'Example',
    }
  );
 
  # Таблицу нужно выводить отдельно
  print $table->show();
 
  # Сообщаем движку, что функция завершилась нормально
  return 1;
}

Работа с БД

Все классы работы с БД наследуются от dbcore.
В таком случае в классе становятся доступны следующие методы:

query($query, $type, $attr) выполнение запроса к БД ( В основном используется для операции SELECT )
changes($table, $data, $attr) обёртка над query(«UPDATE …»). Сравнивает данные в таблице и изменяет только поля с обновлёнными значениями. Может добавлять в системный лог записи об изменении
query_add($table, $data, $attr) обёртка над query(«INSERT …»). Добавляет данные в таблицу, инкапсулирует логику обработки значений некоторых типов (ip, netmask, attachment, reply, text…)
query_del($table, $data, $extended_params, $attr) обёртка над query(«DELETE …»), В нормальном случае используется для удаления строки с id = $data→{ID}
search_former($search_columns, $attr) специальный метод формирования WHERE части запроса.

Все эти методы должны вызываться в объекте с заданными полями conf, db, admin ($self→{db}, $self→{conf}, $self→{admin}).

Конструктор в общем случае должен реализовать как минимум этот функционал

#**********************************************************
=head2 new($db, $admin, \%conf) - Constructor for Example
 
=cut
#**********************************************************
sub new{
  my ($class, $db, $admin, $CONF) = @_;
  my $self = {
    db    => $db,
    admin => $admin,
    conf  => $CONF
  };
 
  bless($self, $class);
  return $self;
}

Рассмотрим работу с каждым из унаследованных методов детальнее.

**Таблица, которая используется в запросах**

CREATE TABLE `example_entity` (
  `id` INT UNSIGNED PRIMARY AUTO_INCREMENT,
  `name` VARCHAR(40) NOT NULL,
  `value` SMALLINT(6) NOT NULL DEFAULT
);
id name value
1 name1 101
2 name2 102

query($query, $type, $attr)

Метод query() выполняет запрос к базе и в зависимости от аргумента $type получает результат и в зависимости от значений в $attr применяет к нему некоторые преобразования.

Рассмотрим примеры запросов и результат выполнения.

  $self->query("SELECT * FROM example_entity");

Результатом выполнения будет запись в $self→{list} двумерного масива содержимого таблицы example_entity.

  $self->query("SELECT * FROM example_entity", undef, { COLS_NAME => 1 })

Здесь в качестве $type мы указываем undef для получения данных из базы. $attr→{COLS_NAME} ⇒ 1 говорит, что мы хотим получить результаты в виде масива хешей. Результатом выполнения будет запись в $self→{list} масива хешей, где ключами хеша будут названия столбцов таблицы, а значениями - соответственно значения.
Поскольку структура таблицы (порядок столбцов в таблице) может меняться, использование COLS_NAME предпочтительнее (Читать как: Использовать всегда и везде при получении списков базы)

Следующий пример удобен, когда в коде нам нужно будет сформировать простой список выбора или поисковую таблицу ключ - значение. (Например, по id строки)

  $self->query("SELECT id,name FROM example_entity", undef, { LIST2HASH => 'id,name' });

Результатом выполнения запроса будет запись в $self→{list_hash} хеша, где ключ id строки, а значение name.

Теперь рассмотрим ключ COLS_UPPER.

  $self->query("SELECT * FROM example_entity", undef, { COLS_NAME => 1, COLS_UPPER => 1 })

Использование этого ключа связано с системой шаблонов, по утверждённому стандарту, названия столбцом таблицы указываются в lowercase, а переменные шаблона указываются в UPPERCASE. Таким образом, для передачи данных из БД в шаблон, пришлось бы вручную переназначать переменные при передаче в шаблон. Ключ COLS_UPPER дублирует ключи в хеше в в виде UPPERCASE, что позволяет передавать строки результата в шаблон без дополнительной логики.

Создание базовой страницы

Создаем базовую страницу сервиса

cgi-bin/hello.cgi

#!/usr/bin/perl
=head1 NAME

 Hello  world

=cut

use strict;
use warnings;

# Включение нужных путей
BEGIN {
  our $libpath = '../';
  my $sql_type = 'mysql';
  unshift(@INC,
    $libpath . "Abills/$sql_type/",
    $libpath . "Abills/modules/",
    $libpath . '/lib/',
    $libpath . '/Abills/',
    $libpath
  );
}

#Модуль конфигурации
use Conf;
our (
  $libpath,
  %conf,
  %lang,
  $base_dir,
);

# конфигурационный файл
do "../libexec/config.pl";

# HTML визуализация
use Abills::HTML;
my $html = Abills::HTML->new(
  {
    IMG_PATH => 'img/',
    NO_PRINT => 1,
    CONF     => \%conf,
    CHARSET  => $conf{default_charset},
  }
);

# Подключение базы
use Abills::SQL;
my $db = Abills::SQL->connect($conf{dbtype}, $conf{dbhost}, $conf{dbname}, $conf{dbuser}, $conf{dbpasswd}, {
  CHARSET => ($conf{dbcharset}) ? $conf{dbcharset} : undef
});

# Включение базовых словарей
if($html->{language} ne 'english') {
  do $libpath . "/language/english.pl";
}

if(-f $libpath . "/language/$html->{language}.pl") {
  do $libpath."/language/$html->{language}.pl";
}

# Подключение модуля работы с шаблонами 
require Abills::Templates;

# Включение конфигурационного файла
Conf->new($db, undef, \%conf);

$html->{METATAGS} = templates('metatags_client');

print $html->header();

# Диалоговое окно приветсвия
print $html->message('info', $lang{INFO}, "Hello world\nSystem name '$conf{WEB_TITLE}'");

1;