
Файлы и порядок миграций
Имя файла
Новая конвенция:
YYYYMMDD_HHMM__slug.phpНапример:
20260810_1530__add_order_status.phpПорядок выполнения — лексикографическая сортировка полного имени. Генератор берёт более позднее из двух значений: текущую минуту или последнюю метку в каталоге плюс одна минута. Это защищает от разных часовых поясов, повторной генерации в одну минуту и отстающих часов машины.
Не добавляйте секунды в метку
Формат YYYYMMDD_HHMMSS несовместим с существующими минутными метками при строковой сортировке. Для создания файлов используйте команду new.
Сканер читает только обычные .php-файлы в корне migrations_path. Скрытые файлы, подкаталоги, migrate.php, README.md и файлы других типов игнорируются.
Контракт PHP-миграции
Обычный файл возвращает callable, который принимает поднятый экземпляр MODX. Для MODX 2:
<?php
return function (modX $modx): void {
$prefix = (string)$modx->getOption('table_prefix', null, 'modx_');
$table = $prefix . 'site_orders';
// Сначала проверить текущее состояние, затем привести его к целевому.
echo 'Готово.' . PHP_EOL;
};Для MODX 3 меняется тип аргумента:
<?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, даже если успела частично изменить базу. Поэтому тело должно различать три ситуации:
- Целевое состояние уже достигнуто — завершиться успешно без повторного изменения.
- Изменение ещё не сделано — выполнить его и проверить результат.
- Выполнить изменение невозможно — остановиться с ошибкой. Например, миграция должна изменить тип колонки
status, но таблицы или самой колонки нет. Молча пропускать такую ситуацию нельзя: раннер отметит файл применённым, хотя база не достигла нужного состояния.
Пример добавления колонки для MODX 2:
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.
