Skip to content
mxMigrations
mxMigrations
Расширяемый раннер и генератор миграций для MODX Revolution 2 и 3 — журнал, контрольные суммы, защита порядка и готовые шаблоны.
  1. Компоненты
  2. mxMigrations
  3. Использование
  4. Файлы и порядок миграций

Файлы и порядок миграций

Имя файла

Новая конвенция:

text
YYYYMMDD_HHMM__slug.php

Например:

text
20260810_1530__add_order_status.php

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

Не добавляйте секунды в метку

Формат YYYYMMDD_HHMMSS несовместим с существующими минутными метками при строковой сортировке. Для создания файлов используйте команду new.

Сканер читает только обычные .php-файлы в корне migrations_path. Скрытые файлы, подкаталоги, migrate.php, README.md и файлы других типов игнорируются.

Контракт PHP-миграции

Обычный файл возвращает callable, который принимает поднятый экземпляр MODX. Для MODX 2:

php
<?php

return function (modX $modx): void {
    $prefix = (string)$modx->getOption('table_prefix', null, 'modx_');
    $table = $prefix . 'site_orders';

    // Сначала проверить текущее состояние, затем привести его к целевому.

    echo 'Готово.' . PHP_EOL;
};

Для MODX 3 меняется тип аргумента:

php
<?php

return function (\MODX\Revolution\modX $modx): void {
    $prefix = (string)$modx->getOption('table_prefix', null, 'modx_');

    // Работа миграции.
};

MODX уже загружен раннером. Не запускайте bootstrap ядра повторно и не объявляйте MODX_API_MODE внутри нового файла.

Всё, что миграция печатает, сохраняется в колонке output журнала. Исключение помечает файл как failed; его сообщение вместе с накопленным выводом также попадает в журнал.

Идемпотентность

Failed-миграция запускается повторно при следующем up, даже если успела частично изменить базу. Поэтому тело должно различать три ситуации:

  1. Целевое состояние уже достигнуто — завершиться успешно без повторного изменения.
  2. Изменение ещё не сделано — выполнить его и проверить результат.
  3. Выполнить изменение невозможно — остановиться с ошибкой. Например, миграция должна изменить тип колонки status, но таблицы или самой колонки нет. Молча пропускать такую ситуацию нельзя: раннер отметит файл применённым, хотя база не достигла нужного состояния.

Пример добавления колонки для MODX 2:

php
return function (modX $modx): void {
    $prefix = (string)$modx->getOption('table_prefix', null, 'modx_');
    $table = $prefix . 'site_orders';
    $column = 'status';

    $statement = $modx->query(
        'SELECT COUNT(*) FROM information_schema.COLUMNS'
        . ' WHERE TABLE_SCHEMA = DATABASE()'
        . ' AND TABLE_NAME = ' . $modx->quote($table)
        . ' AND COLUMN_NAME = ' . $modx->quote($column)
    );

    if ($statement !== false && (int)$statement->fetchColumn() > 0) {
        echo 'Колонка уже существует.' . PHP_EOL;
        return;
    }

    $sql = 'ALTER TABLE ' . $modx->escape($table)
        . ' ADD COLUMN ' . $modx->escape($column)
        . " VARCHAR(20) NOT NULL DEFAULT 'new'";

    if ($modx->exec($sql) === false) {
        throw new RuntimeException('Не выполнился запрос: ' . $sql);
    }
};

В линии MODX 3 тело то же, но сигнатура начинается с return function (\MODX\Revolution\modX $modx): void.

Состояния плана

ГруппаЧто означаетЧто делать
pendingЗаписи нет либо предыдущая попытка имеет статус failed.Проверить dry-run и выполнить up.
appliedФайл записан как applied или baseline, checksum совпадает.Ничего.
driftedПрименённый файл изменился.Вернуть исходный файл или осознанно принять checksum.
missingЗапись осталась, файл удалён или переименован.Вернуть файл с прежним именем.
outOfOrderНовый датированный файл оказался раньше уже применённых.Дать неприменённому файлу более позднюю метку.

Переименование применённой миграции одновременно создаёт missing для старого имени и pending для нового. Имя — часть неизменяемой истории.

Журнал

Таблица создаётся автоматически перед чтением плана. Одна строка содержит:

  • имя файла и SHA-256 нормализованного содержимого;
  • Unix-время и автора применения;
  • длительность в миллисекундах;
  • статус applied, baseline или failed;
  • вывод либо сообщение ошибки.

Перед SHA-256 раннер удаляет UTF-8 BOM и приводит CRLF/CR к LF. Одна лишь смена окончаний строк между Windows и Linux не создаёт ложный drift.

Запись уникальна по имени файла. Повторная попытка failed-миграции обновляет ту же строку.

Блокировка

up и baseline используют MySQL GET_LOCK(..., 0): если другой процесс уже держит блокировку, команда немедленно отказывает. После завершения выполняется RELEASE_LOCK; при аварийном завершении MySQL освободит lock вместе с соединением.

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

Legacy-файлы

Если PHP-файл содержит MODX_API_MODE, раннер считает его старой автономной миграцией и запускает отдельным PHP-процессом. Это работает в обеих линиях: legacy-файл сам выполняет bootstrap своей версии MODX. Код возврата должен быть 0; stdout и stderr сохраняются в журнале.

Эта совместимость нужна для существующей истории. Новые миграции должны возвращать callable и использовать уже загруженный $modx.

Чего раннер не делает

  • не выполняет .sql-файлы;
  • не выполняет автоматический rollback;
  • не оборачивает произвольный DDL в транзакцию;
  • не удаляет записи missing из журнала;
  • не исправляет drift без явного --ignore-checksum.