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

Корневые скрипты — это точки входа вашего сайта. Как правило, это index.php и cron.php, а для AI-агента — agent.php. Их задача минималистична: инициализировать платформу, определить, какой модуль нужно запустить, и опубликовать результат. Никакой бизнес-логики в корневых скриптах нет — она вся находится в модулях.

Единственное ручное подключение файла во всём проекте — units/melbis.php первой строкой. Что именно он инициализирует, описано в разделе «melbis.php».

index.php

Разберём структуру типового корневого скрипта построчно:

// Melbis start
require 'units/melbis.php';

Подключение units/melbis.php инициализирует всё окружение платформы: загружает config.json, настраивает обработчики ошибок, регистрирует автозагрузчик классов и готовит соединение с базой данных. Само соединение открывается при первом запросе к базе, поэтому страница, целиком взятая из кеша, обходится без него. После этой строки доступна функция MELBIS(), возвращающая единственный экземпляр парсера. Подробнее о том, что делает melbis.php — в конце этого раздела.


// Define session
MELBIS()->DefineSession('MELBIS_SHOP');

Инициализация PHP-сессии. Параметр — уникальное имя сессии для этого проекта. Помимо сессии, этот вызов автоматически активирует CSRF-токен и проверяет наличие в URL параметра отладчика: если передан ?debug_on_KEY и KEY совпадает с MELBIS_DEBUG_CODE из конфигурации — включается режим отладки. Полная сигнатура и работа с сессией — в разделе «Сессии и CSRF».


// Define self constants
MELBIS()->DefineSelfConst();

Загружает пользовательские константы в пространство PHP-констант. Вызов необязателен, если в проекте нет пользовательских констант. Подробнее — в разделе «Конфигурация».


// Entry point
if ( isset($_GET['lazy']) )
{
    $entry_point = $_POST['mod'];
    $entry_param = $_POST['params'];
}
else
{
    $entry_point = $_GET['mod'] ?? 'melbis_base_page';
    $entry_param = [serialize($_GET), serialize($_POST)];
}

Определение точки входа — имени модуля, который будет запущен первым. Логика проста:


// Entry point check
$entry_exists = MELBIS()->UnitExists($entry_point, true);
if ( !$entry_exists )
{
    header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
    $entry_point = 'melbis_base_404';
}

Имя модуля пришло из адреса, то есть его выбрал не разработчик, а посетитель — и совпадать с реальным модулем оно не обязано. UnitExists($mName, $mCheckEntry = false) проверяет, что модуль с таким именем в проекте есть, и возвращает true или false.

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

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

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

Та же проверка нужна везде, где имя модуля приходит снаружи: в обработчике cron-запросов и в скрипте отложенной загрузки.


// Run
MELBIS()->Run($entry_point, $entry_param);

// Publish
MELBIS()->Publish();

// Possible report
MELBIS()->Report();

Финальная тройка вызовов присутствует в каждом корневом скрипте:

Полный пример index.php

<?php

// Melbis start
require 'units/melbis.php';

// Define session
MELBIS()->DefineSession('MELBIS_SHOP');

// Define self constants
MELBIS()->DefineSelfConst();

// Entry point
if ( isset($_GET['lazy']) )
{
    $entry_point = $_POST['mod'];
    $entry_param = $_POST['params'];
}
else
{
    $entry_point = $_GET['mod'] ?? 'melbis_base_page';
    $entry_param = [serialize($_GET), serialize($_POST)];
}

// Entry point check
$entry_exists = MELBIS()->UnitExists($entry_point, true);
if ( !$entry_exists )
{
    header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
    $entry_point = 'melbis_base_404';
}

// Run
MELBIS()->Run($entry_point, $entry_param);

// Publish
MELBIS()->Publish();

// Possible report
MELBIS()->Report();

Подключение библиотеки

Обычно библиотеки подключаются галочками в параметрах модуля через IDE — так парсер знает не только сами файлы, но и таблицы, от которых они зависят. Корневой скрипт модулем не является, и такой галочки у него нет: если функция библиотеки нужна прямо здесь, до Run, её подключают вызовом UnitInc:

MELBIS()->UnitInc('melbis_inc_logic');

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

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

Корневой скрипт по шагам

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

// Define lang
MELBIS()->UnitInc('melbis_inc_index_lang');

// Run
MELBIS()->UnitInc('melbis_inc_index_run');

// Log
MELBIS()->UnitInc('melbis_inc_index_log');

Такой inc-модуль — обычный модуль-библиотека, созданный в среде разработки и видимый в общем дереве. Отличие только в том, что он не объявляет функции, а выполняет код сразу при подключении.

Переменные из подключённого файла наружу не выходят. Подключение выполняется внутри метода парсера, поэтому код файла работает в его области видимости, а не в области видимости index.php. Всё, что должно пережить подключение, объявляется в самом файле через global:

global $gLang, $gLangPath;

Без этого переменная тихо останется пустой — ошибки не будет, поломка обнаружится позже и в другом месте.

cron.php

Если проект использует периодические задачи — рассылки, очистку данных, автоматические импорты, — рядом с index.php появляется второй корневой скрипт, cron.php. Он так же начинается с require 'units/melbis.php', но вместо Run объявляет расписание методом CronAdd и запускает подошедшие задачи вызовом CronRun. Прямой доступ к нему из браузера закрыт правилом .htaccess.

Формат расписания, устройство cron-модуля, защита от вызова извне, журналы задач и обработка ошибок — в разделе «Планировщик задач».

agent.php

Третий корневой скрипт — точка входа AI-агента. Через неё MCP-сервер запускает модули агента: так агент делает работу, для которой в магазине нет AI-инструмента, — отчёт по нескольким таблицам, проверку всего каталога, разовую правку со своей логикой. Скрипт поставляется с магазином, и править его не нужно.

Как и index.php, он начинается с require 'units/melbis.php', а затем запускает модуль, только если:

  1. ключ в запросе совпадает с тем, что магазин выдал логину при входе агента;
  2. имя модуля начинается с agent_;
  3. модуль есть в units/;
  4. модуль — точка входа: entry_point: 1 в манифесте, в программе — «Модуль входной точки».

Иначе скрипт отвечает отказом с кодом 403 или 404 и ничего не запускает. Служебные поля запроса — login, secret и mod — он убирает, а остальные отдаёт модулю одним сериализованным аргументом: в манифесте модуля объявлено post: serial, и значения лежат в $mVars['post']. Ответ модуля проходит через парсер, как любая страница.

С магазином поставляется образец — units/agent_claude_sample.php. Он показывает, что точка входа передаёт модулю, и на нём агент проверяет канал:

<?php
namespace AGENT_CLAUDE_SAMPLE;

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

    $hello = $post['hello'] ?? '';
    $how = (int)( $post['how'] ?? 0 );

    $keys = array_keys($mVars);
    $post_keys = array_keys($post);

    $answer = [
        'time'      => date('Y-m-d H:i:s'),
        'nick'      => MELBIS_DB_NICK,
        'keys'      => $keys,
        'post_keys' => $post_keys,
        'hello'     => $hello,
        'doubled'   => $how * 2
        ];

    return json_encode($answer, JSON_UNESCAPED_UNICODE);
}
?>

Запуск с параметрами hello: мир и how: 21 отвечает так:

{"time": "2026-10-03 12:47:00", "nick": "ms", "keys": ["post"],
 "post_keys": ["hello", "how"], "hello": "мир", "doubled": 42}

Как агент создаёт и запускает свои модули — «Витрина» и «shop_agent» в описании MCP-сервера.

.htaccess

Файл .htaccess решает несколько задач одновременно. Минимальный набор правил, который должен присутствовать в каждом проекте:

AddDefaultCharset UTF-8
DirectoryIndex index.html index.htm index.php
Options -Indexes

# Запрет прямого доступа к конфигурации
RewriteRule ^config.json$ - [F]

# Запрет прямого доступа к cron
RewriteRule ^cron.php$ - [F]

# Запрет прямого доступа к внутренним папкам движка
RewriteRule ^core/(cache|class|init|tmp|trick|log)/ - [F]

# Запрет прямого доступа к журналам в любом месте проекта
RewriteRule \.log$ - [F]

# Маршрут для отложенной загрузки модулей (Lazy Loading)
RewriteRule ^lazy/$ index.php?lazy [L,QSA]

Правило ^core/(…)/ закрывает всё, что движок держит для себя: классы (class), скомпилированный кеш (cache), данные аналитики (trick), временные файлы (tmp), SQL-скрипты установки (init) и журналы (log). Снаружи там не нужно ничего и никогда. Точки входа core/*.php, через которые с движком общается программа Melbis Shop, правило не затрагивает — журналы читаются в дереве скриптов программы, а не по адресу в браузере.

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

Правило lazy/ обязательно, если в проекте используется отложенная загрузка модулей — оно перенаправляет все AJAX-запросы платформы на index.php с признаком ?lazy.

При необходимости здесь же размещают ЧПУ-маршруты, HTTPS-редиректы и правила кеширования статики.

Ошибка в .htaccess ломает весь сайт сразу, вместе с точками входа core/*.php, через которые его можно было бы исправить. Поэтому движок, сохранив .htaccess из программы или от AI-агента, тут же спрашивает свой сайт с того же сервера: главную страницу и core/mcp.php. Файл остаётся, только если главная ответила нормально, а core/mcp.php — кодом 200. Ошибка, закрытая точка входа или молчание сервера возвращают прежний файл, и сохранение получает отказ.