2. Руководство разработчика › 2.3 Скрипты › Модули-библиотеки

Модули-библиотеки — это модульные скрипты группы inc. В отличие от обычных модулей, парсер их не вызывает и шаблонов у них нет. Их задача — содержать общие функции, которые используются сразу несколькими обычными модулями. Это позволяет вынести повторяющуюся логику в одно место и не дублировать её в каждом модуле отдельно.

В имени библиотеки после inc идёт её собственная группа — melbis_inc_page_callback.php относится к группе page. По ней библиотеки и раскладываются в дереве «Среды разработки», рядом с модулями своей части проекта (см. «Принятые обозначения»).

Чтобы подключить библиотеку к обычному модулю, достаточно поставить галочку напротив неё в правой панели IDE — парсер загрузит её автоматически перед запуском модуля, и все её функции станут доступны.

Библиотека подключает другие библиотеки теми же галочками. Парсер загружает их по дереву подключений, каждую один раз, и всё, от чего библиотека зависит, загружено раньше её самой. Взаимные галочки двух библиотек загрузчик допускает, и функции друг друга они вызывают без помех. Но первой в таком круге загружается та, до которой обход дошёл позже, а это зависит от галочки модуля. Поэтому код в теле файла такой библиотеки, вне функций, не вызывает функций другой: в одном модуле он сработает, в другом остановится на Call to undefined function.

Пространство имён и короткое имя

Библиотека, как и обычный модуль, объявляет собственное пространство имён — имя своего файла в верхнем регистре — и называет функции коротко, без приставок (см. «Модульные скрипты»):

<?php
namespace MELBIS_INC_LOGIC_ORDER;

function Create()
{
    ...
}

Вторая половина дела — короткое имя, под которым библиотеку будут звать. Его объявляет сама библиотека, и потому оно одинаково во всём магазине: LOGIC_ORDER\Create читается одинаково в любом файле. Пишется имя в поле параметров над редактором: у обычного модуля там объявлены входные параметры, а у библиотеки их нет, и поле занято именно этим.

Имя одно. Список через запятую движок при сохранении не примет и ответит One alias only, показав, что написано в поле. Одинаковые имена у разных библиотек не запрещены: столкнутся они только в файле, который подключил обе, и там решает автор файла.

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

Заполнить имя, но не объявить namespace, нельзя: при сохранении движок предупредит Namespace missing, потому что импортировать в таком случае нечего.

Объявленное имя — рекомендация для всего магазина, а не запрет. Модуль вправе дать библиотеке в своём файле любое имя, но при сохранении получит Alias differs с обоими вариантами: в файле и в манифесте библиотеки. Предупреждение не мешает сохранению — оно нужно, чтобы одну библиотеку в разных файлах не звали по-разному, когда на то нет причины.

Библиотеку, которую модуль подключает, но не вызывает, объявляют строкой use с полным именем и без as. Такая библиотека работает самим подключением — например, код в теле её файла регистрирует колбэки для вложенных модулей. Без этой строки движок при сохранении предупредит Unused: библиотека подключена, а ни одна её функция в модуле не вызывается.

Рассмотрим три характерных примера из демонстрационного магазина.

melbis_inc_page_menu — временные таблицы

Короткое имя этой библиотеки — PAGE_MENU. Её функция Sub строит временную таблицу в памяти со всеми подразделами заданного раздела, рекурсивно обходя дерево категорий. Вторая функция, Menu, отдаёт разделы одного меню — её зовут melbis_cataloge и melbis_cataloge_sub.

namespace MELBIS_INC_PAGE_MENU;

function Sub($mId)
{
    $command = "CREATE TEMPORARY TABLE {DBNICK}_topic_sub ENGINE=MEMORY
                WITH RECURSIVE topic_sub AS (
                    SELECT t.tindex, t.id
                      FROM {DBNICK}_topic t
                     WHERE t.id = :ID
                     UNION ALL
                    SELECT ts.tindex, t.id
                      FROM topic_sub ts
                      JOIN {DBNICK}_topic t ON ts.id = t.tindex
                )
                SELECT * FROM topic_sub";

    $param = [
        'id' => $mId
        ];
    MELBIS()->SqlQuery(__LINE__, $command, $param);
}

Зачем это нужно? Когда модуль выводит список товаров в разделе, он должен учитывать не только товары самого раздела, но и товары всех его подразделов. Та же таблица понадобится модулю фильтров по характеристикам. Вместо того чтобы писать рекурсивный CTE в каждом из этих модулей, достаточно один раз вызвать PAGE_MENU\Sub из библиотеки — и временная таблица готова к использованию в последующих запросах:

// В модуле melbis_page_topic:
namespace MELBIS_PAGE_TOPIC;

use MELBIS_INC_PAGE_MENU as PAGE_MENU;

function Main($mVars)
{
    $id = $mVars['id'];

    // Создать временную таблицу подразделов
    PAGE_MENU\Sub($id);

    // Теперь можно делать JOIN с {DBNICK}_topic_sub
    $command = "SELECT s.id
                  FROM {DBNICK}_topic_sub t_sub
                  JOIN {DBNICK}_topic t
                    ON t_sub.id = t.id
                  JOIN {DBNICK}_topic_store ts
                    ON ts.topic_id = t.id
                  JOIN {DBNICK}_store s
                    ON ts.store_id = s.id
                 WHERE s.no_visible = 0
              GROUP BY s.id
              ORDER BY MIN(t.absindex), MIN(ts.pos)
                 LIMIT 100
                ";
    $goods = MELBIS()->SqlSelect(__LINE__, $command);
    ...
}

melbis_inc_page_callback — колбэки шаблонизатора

Этот модуль регистрирует модификаторы шаблонизатора. Колбэк — это PHP-функция, которую можно вызывать прямо из HTML-шаблона как модификатор переменной.

Регистрация стоит в теле файла библиотеки, а колбэки — ниже, рядом с ней:

// В библиотеке melbis_inc_page_callback:
namespace MELBIS_INC_PAGE_CALLBACK;

// Register the template callbacks
MELBIS()->DefineCallback('TopicLink');
MELBIS()->DefineCallback('StoreLink');
MELBIS()->DefineCallback('StatusName');

function TopicLink($mVars)
{
    $link = ( $mVars['kind_key'] == 'kLink' ) ? $mVars['link'] : '/?topic_id='.$mVars['id'];

    return $link;
}

Имя функции в регистрации не написано: движок ищет TopicLink в том же файле, где стоит строка регистрации, — здесь это MELBIS_INC_PAGE_CALLBACK\TopicLink, а в библиотеке без пространства имён — MELBIS_INC_PAGE_CALLBACK_TopicLink. Поэтому короткая регистрация стоит в самой библиотеке: в файле модуля та же строка искала бы функцию модуля.

Назвать функцию явно нужно, только если её имя отличается от имени модификатора или она лежит в другом файле:

MELBIS()->DefineCallback('TopicLink', PAGE_CALLBACK\TopicLink(...));

Три точки — не сокращение записи в примере, а синтаксис PHP: так функция передаётся как значение, не вызываясь, и имя разрешает компилятор вместе со строкой use, а не строка в кавычках. Строковая форма ('MELBIS_INC_PAGE_CALLBACK\TopicLink') тоже работает, но короткого имени из use она не знает: имя в кавычках пишут полностью, и при переезде функции оно остаётся прежним. Любую из форм движок проверяет сразу при регистрации, так что несуществующее имя до витрины не доживёт.

Тело файла библиотеки выполняется один раз за запрос — когда её подключает первый модуль. Модуль подключается на каждом запросе, даже когда его результат берётся из кеша, поэтому регистрация стоит в теле файла, а не внутри функции: функция при отдаче из кеша не запускалась бы вовсе.

Библиотеку подключает каждый модуль, в чьём шаблоне стоит модификатор: галочкой в правой панели и строкой use с полным именем и без as. Функции библиотеки модуль не вызывает — она работает самим подключением:

// В модуле меню разделов melbis_cataloge:
namespace MELBIS_CATALOGE;

use MELBIS_INC_PAGE_MENU as PAGE_MENU;
use MELBIS_INC_PAGE_CALLBACK;

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

Реестр колбэков общий на весь запрос: модуль получает те колбэки, что зарегистрированы к моменту его подключения. Собственные библиотеки модуля подключаются раньше него самого, поэтому своя строка use успевает всегда.

В шаблоне такого модуля доступен модификатор $TopicLink, который вычисляет правильный URL раздела в зависимости от его типа:

{#MENU}
    <a href="{ID|$TopicLink:KIND_KEY,LINK}">{NAME|html}</a>
{MENU#}

Имя модификатора — регистрозависимый ключ. По соглашению оно совпадает с именем функции: одно имя ищется поиском во всех трёх местах — в шаблоне, в регистрации и в объявлении. Заодно в теге читаются три класса имён: заглавные ID, KIND_KEY — данные, строчные path, webp — встроенные модификаторы, $TopicLink — функция модуля.

Так это сделано в демонстрационном магазине: $TopicLink стоит в шаблонах меню melbis_cataloge и подменю melbis_cataloge_sub, $StoreLink и $StatusName — в карточке товара melbis_store_card и на странице товара melbis_page_store, и каждый из них подключает библиотеку сам. Подменю зовут и отдельным запросом /?mod=melbis_cataloge_sub, без страницы вокруг, а меню разделов выводится и в шапке веб-модуля melbis_web_sample. Роутер страницы melbis_base_page библиотеку не подключает: в его шаблонах модификаторов нет.

Колбэк, читающий базу. Таблицы, которые колбэк читает через двери движка, попадают в зависимости кеша того модуля, в чьём шаблоне он сработал. Какое чтение колбэку допустимо — «Модификаторы» → «Колбэк и база данных».

Подробнее о модификаторах и синтаксисе колбэков — в разделе «Модификаторы».

melbis_inc_logic_* — единая бизнес-логика

Это наиболее значимый тип библиотечного модуля. Работа с заказом в демонстрационном магазине разложена по библиотекам группы logic: создание и загрузка заказа, добавление и удаление товаров, скидки, расчёт, запись:

Библиотека Короткое имя Функции
melbis_inc_logic_order LOGIC_ORDER версия заказа: Create — новая, Load — текущая, GoodsAdd и GoodsRemove — добавить и удалить товар, GoodsSum — сумма товаров, GoodsDiscount — скидка на товар, OptionSet — опция заказа и другие
melbis_inc_logic_order_calc LOGIC_ORDER_CALC Run — рассчитать версию заказа
melbis_inc_logic_order_edit LOGIC_ORDER_EDIT Run — записать версию заказа
melbis_inc_logic_common LOGIC_COMMON Rate — курс валюты, Price — сумма в валюте магазина

Эти функции вызываются из модуля корзины на витрине сайта:

// В модуле melbis_basket:
namespace MELBIS_BASKET;

use MELBIS_INC_LOGIC_ORDER as LOGIC_ORDER;
use MELBIS_INC_LOGIC_ORDER_CALC as LOGIC_ORDER_CALC;

...
$version = MELBIS()->SessionGetValue('order') ?? LOGIC_ORDER\Create();
$version = LOGIC_ORDER\GoodsAdd($version, $store_id);
$version = LOGIC_ORDER_CALC\Run(null, $version);

Ключевое преимущество этого подхода — единая бизнес-логика для сайта и настольной программы. Те же самые функции библиотек logic вызываются программой Melbis Shop, когда менеджер работает с заказами через Windows-клиент. Настройка вызываемых функций выполняется в программе через меню «Проектирование → Реестр настроек», вкладка «Базовые настройки», раздел «Вызываемые модули». Например, в разделе «Заказы → Калькуляция» указывается библиотека melbis_inc_logic_order_calc.php и функция, которую программа должна вызывать для расчёта заказа, — MELBIS_INC_LOGIC_ORDER_CALC\Run. Имя функции хранится строкой и пишется полностью, с пространством имён: код о нём не знает, и при переезде функции его правят руками.

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

Другие примеры библиотек

Помимо перечисленных, в проектах на платформе Melbis типичны и другие библиотеки:

Любую логику, которая нужна более чем в одном модуле, стоит выносить в библиотеку.