Глобальный массив

Обычно каждый модуль парсит только те данные, которые сам сгенерировал (локальный контекст). Но в сложных интерфейсах данные из одного модуля часто нужны в другом (например, модуль Корзины посчитал сумму, а вывести ее нужно в шапке сайта).

Для этого существует отдельный глобальный массив. Работа с ним идёт через четыре метода.

Они устроены точно так же, как Tpl*. GlobalAssign — это TplAssign, GlobalAppendTplAppend, и так далее: те же формы вызова, то же приведение ключей к верхнему регистру, те же правила слияния, тот же путь через :. Отличие ровно одно — не нужен указатель контекста: глобальный массив на всю страницу один. Если вы разобрались с одной парой методов, вторая не потребует ничего нового.

GlobalAssign($name, $value = '') — записывает значение под указанным именем, полностью затирая то, что там было раньше: и скаляр, и массив. Простое присваивание, никаких слияний.

Вызывается в двух формах — пара «имя + значение» или один массив пар:

// Имя + значение
MELBIS()->GlobalAssign('site_currency', 'UAH');
MELBIS()->GlobalAssign('cart', ['total_sum' => 1500]);

// Массивом пар: каждый ключ становится отдельной переменной
MELBIS()->GlobalAssign(['cart' => $cart_data, 'user' => $user_data]);

При передаче массивом пар второй аргумент не используется.

Ключи приводятся к верхнему регистру — и имя переменной, и все строковые ключи внутри переданного массива, на любую глубину. В PHP пишите как удобно, в шаблоне ключ всегда в UPPERCASE: GlobalAssign('cart', ['total_sum' => 1500]){CART:TOTAL_SUM}. Числовые ключи списков остаются как есть.

GlobalAppend($name, $value = '', $replace = false) — не затирает, а объединяет с уже существующим значением, и делает это по-разному в зависимости от типа $value:

Если тип не совпал, старое значение теряется. Дописать массив туда, где лежит скаляр, — скаляр отбрасывается, слияние начинается с пустого массива. Дописать скаляр туда, где лежит массив, — массив отбрасывается, остаётся только новая строка. Ошибки при этом не будет.

Формы вызова те же, но с одной особенностью: при передаче массивом пар роль $replace играет второй аргумент, а не третий.

// Имя + значение
MELBIS()->GlobalAppend('page', ['title' => 'Каталог товаров']);

// Массивом пар: второй аргумент — $replace
MELBIS()->GlobalAppend(['cart' => $extra_cart, 'user' => $extra_user]);
MELBIS()->GlobalAppend(['cart' => $extra_cart_data], true); // одноуровневая замена вместо слияния

Типичный случай для Append: несколько модулей независимо друг от друга пишут под одним именем. Библиотека авторизации кладёт page = ['auth' => 1, 'user_id' => 5], а модуль страницы хочет добавить туда же заголовок:

MELBIS()->GlobalAppend('page', ['title' => 'Каталог товаров']);
// -> page = ['auth' => 1, 'user_id' => 5, 'title' => 'Каталог товаров']

GlobalFetch($name, $default = null) — читает значение обратно уже на стороне PHP (например, чтобы прочитать то, что записал в глобальный массив другой модуль). Принимает и путь: GlobalFetch('PAGE:DIR') достанет вложенный элемент, ничего при этом не создавая. Поведение при отсутствующем ключе — как у TplFetch: без второго аргумента — ошибка разбора, с любым вторым аргументом (в том числе null) — тихо вернёт его вместо ошибки.

GlobalClear($name) — удаляет значение из глобального массива. Принимает как одно имя, так и массив имён: GlobalClear(['CART', 'USER']) удалит обе переменные разом. В любом из них можно указать путь: GlobalClear('PAGE:DIR') удалит один вложенный ключ, оставив остальной PAGE нетронутым. Несуществующий путь просто игнорируется.

Обращение по вложенному пути

Все четыре метода понимают путь через : — ту же нотацию, что используется на чтение в шаблонах ({PAGE:DIR}). Метод адресует не всю переменную целиком, а один вложенный элемент, не трогая соседей.

Различаются они только тем, создают ли недостающие уровни пути: пишущие создают, читающие — нет. GlobalAssign и GlobalAppend достроят структуру до нужного листа, а GlobalFetch и GlobalClear по несуществующему пути ничего не создадут: первый вернёт ошибку разбора или значение по умолчанию, второй просто ничего не сделает.

// Вместо «прочитать весь PAGE, поправить DIR в PHP, записать PAGE обратно целиком»
MELBIS()->GlobalAssign('PAGE:DIR', $url);
// затронут только PAGE:DIR — остальные ключи PAGE остаются как были

Путь работает и ключом внутри массива пар, когда нужно разложить значения по разным веткам одним вызовом:

MELBIS()->GlobalAssign([
    'PAGE:DIR'  => $url,
    'PAGE:LANG' => $lang,
    'USER:NAME' => $client_name
    ]);

Регистр не важен: путь целиком приводится к верхнему регистру, 'page:dir' и 'PAGE:DIR' — одно и то же.

Числовые сегменты адресуют элемент списка: GlobalAssign('MENU:ITEMS:0:NAME', 'Главная').

Промежуточные уровни создаются сами — у Assign и Append. Fetch и Clear не создают ничего: при отсутствующем пути Fetch завершится ошибкой разбора (или вернёт значение по умолчанию, если оно передано), а Clear ничего не сделает.

Промежуточный скаляр по пути будет уничтожен. Если в PAGE лежала строка, то GlobalAssign('PAGE:DIR', $url) превратит PAGE в массив, а прежняя строка пропадёт: путь требует, чтобы все уровни выше листа были массивами. Ошибки не будет.

Assign или Append по пути

Разница ровно та же, что и без пути, но по пути видна нагляднее:

MELBIS()->GlobalAssign('page', ['dir' => '/catalog/', 'title' => 'Каталог']);

// Assign по пути — чистая замена листа
MELBIS()->GlobalAssign('PAGE:TITLE', 'Новинки');
// PAGE = ['DIR' => '/catalog/', 'TITLE' => 'Новинки']

// Append по пути — конкатенация текста в том же листе
MELBIS()->GlobalAppend('PAGE:TITLE', ' — страница 2');
// PAGE = ['DIR' => '/catalog/', 'TITLE' => 'Новинки — страница 2']

Assign по пути — заменить лист, Append по пути — дописать к листу. Соседние ключи не страдают ни в том, ни в другом случае.

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

Чтение из шаблона

Прочитать записанное можно из любого шаблона на странице, независимо от того, какой модуль его вывел.

Как это работает:

  1. В коде первого модуля разработчик передаёт данные: MELBIS()->GlobalAssign('cart', ['total_sum' => 1500]);
  2. В коде второго модуля — свои данные, тем же способом: MELBIS()->GlobalAssign('user', ['is_logged' => 1]);
  3. В любом HTML-шаблоне других вложенных модулей на странице эти данные становятся доступны по имени верхнего ключа через двоеточие, строго в UPPERCASE:

🔥 Важное правило приоритетов: Локальные переменные модуля всегда “побеждают” глобальные. Если в глобальном массиве есть ключ TITLE, и текущий модуль тоже передал ключ TITLE, парсер выведет локальное значение из модуля. Это защищает верстку от случайных конфликтов.

🔥 Абсолютно все возможности: Глобальные переменные обрабатываются тем же мощным ядром парсера, что и обычные данные. Для них доступны цепочки модификаторов ({CART:TOTAL_SUM|calc:VALUE*0.9|num:0}), логические условия и циклы:

{#MENU:ITEMS}
    <a href="{URL}">{NAME}</a>
{MENU:ITEMS#}

Парсер использует двухуровневую систему защиты HTML-структуры. Если локальных данных для блока нет, парсер проверяет, является ли этот блок глобальным, чтобы решить его судьбу.

Главное правило разработки: Чтобы парсер понял, что блок (например, {#PAGE:LANGS} или {*USER:IS_LOGGED}) относится к глобальному контексту и его нельзя удалять на первом проходе, его корневой ключ (массив) обязан быть инициализирован в PHP до начала парсинга модуля, хотя бы как пустой элемент. Проще всего сделать это через MELBIS()->GlobalAssign('page', []); в точке входа — так ключ PAGE появится в глобальном массиве ещё до старта первого модуля.


Двухпроходный разбор и кеширование

Архитектура шаблонизатора построена по принципу отложенного вычисления (Deferred Evaluation). Это значит, что обработка страницы происходит в два этапа (прохода). Это решает главную проблему: как кэшировать тяжелые модули, но оставлять в них динамические данные (например, актуальную корзину или имя пользователя).

Как работают два прохода:

  1. Первый проход (Генерация модуля и Кэш):
  1. Второй проход (Глобальный финализатор):

🚀 Пробиваем кэш модуля Именно из-за того, что второй проход не кэшируется, глобальный массив — штатный способ “пробить” кэш тяжёлого модуля точечными динамическими вставками, не отключая кэширование самого блока целиком. Вы можете агрессивно кэшировать любую тяжёлую вёрстку, а актуальные для конкретного посетителя данные — количество товаров в корзине {CART:COUNT}, текущий язык, проверку авторизации {*USER:IS_LOGGED} — доставлять в неё через глобальный массив, и они всегда будут отрабатывать в реальном времени, даже внутри закэшированного блока.

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

<div class="container-fluid">
    <div class="row"> 
        <div class="col-xs-12 col-sm-5 col-md-4 col-lg-4">
            <a href="/{VAR:LANG_PATH}"><img src="{PATH}/images/design/logo.png" class="img-responsive"></a>      
        </div>
        <div class="hidden-xs col-sm-7 col-md-8 col-lg-8" style="position: relative;">        
            {MELBIS:studio_cataloge_phone([VAR:LANG])}
            <img src="{PATH}/images/design/memo.png" class="img-responsive pull-right">
        </div>
    </div> 
</div>

Данные посетителя: сессия и кеш

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

// В некешируемом модуле, определяющем посетителя
$client_id = MELBIS()->SessionGetValue('client_id');
MELBIS()->GlobalAssign('user', [
    'is_logged' => !is_null($client_id),
    'name'      => $client_name
    ]);
{*USER:IS_LOGGED}
    Здравствуйте, {USER:NAME|html}
{USER:IS_LOGGED*}

Так шапку сайта можно кешировать целиком, а приветствие всё равно будет персональным. Подробнее о работе с сессией — в разделе «Сессии и CSRF».