DEV Community

Андрей Викулов (VProger)
Андрей Викулов (VProger)

Posted on • Originally published at viku-lov.ru on

Migrations Plugin: управление миграциями базы данных WordPress

О чём статья

Migrations Plugin — это плагин для WordPress, который позволяет версионировать изменения базы данных и управлять ими через удобный интерфейс. Вместо того чтобы вручную править структуру БД или запускать SQL-скрипты, можно создавать миграции, которые автоматически применяются при обновлении плагина или вручную через админ-панель.

Цель статьи: разобрать функциональность плагина, показать примеры создания миграций и объяснить, как безопасно переносить данные между сайтами.

Репозиторий проекта: GitHub - va-proger/wordpress-migrations-plugin


Проблема, которую решает плагин

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

  • ❌ Нет версионирования изменений БД
  • ❌ Нет автоматического применения изменений при обновлении
  • ❌ Нет возможности откатить изменения
  • ❌ Сложно переносить данные между сайтами
  • ❌ Нет истории выполненных изменений
  • ❌ Риск потерять данные при неправильном выполнении SQL

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


Установка и активация

Требования

  • WordPress 5.0+
  • PHP 8.1+
  • MySQL 5.6+
  • Права администратора

Установка

  1. Скопируйте папку плагина в wp-content/plugins/
  2. Перейдите в ПлагиныУстановленные плагины
  3. Найдите "Migrations Plugin"
  4. Нажмите Активировать

При активации плагин автоматически создаёт таблицу wp_migrations_log для хранения истории выполненных миграций.

Первая настройка

После активации перейдите в ИнструментыМиграции. Здесь можно:

  • Просмотреть список всех миграций
  • Запустить невыполненные миграции
  • Откатить выполненные миграции
  • Просмотреть логи выполнения

Основные возможности

  1. Версионирование изменений БД

Все изменения структуры базы данных хранятся в виде отдельных файлов миграций в папке migrations/. Каждая миграция имеет уникальный номер и описание в имени файла:


migrations/

├── 001\_create\_example\_table.php

├── 002\_add\_status\_column.php

├── 003\_migrate\_old\_data.php

└── ...

Enter fullscreen mode Exit fullscreen mode
  1. Автоматическое выполнение

Миграции можно запускать:

  • Вручную через админ-панель (рекомендуется для продакшена)
  • Автоматически при обновлении плагина (настраивается в коде)
  1. Безопасный перенос сущностей

Плагин предоставляет класс Entity_Migrator для безопасного переноса:

  • Постов с проверкой существования по ID, slug, мета-полям
  • Пользователей с автоматическим обновлением существующих
  • Терминов (категории, теги, таксономии)
  • Мета-данных и связей с терминами
  1. Откат изменений

Каждая миграция должна иметь метод down() для отката изменений. Откат выполняется в обратном порядке выполнения миграций.

  1. Логирование

Все выполненные миграции записываются в таблицу wp_migrations_log с информацией о:

  • Времени выполнения
  • Длительности выполнения
  • Статусе (completed/failed)
  • Сообщениях об ошибках

Создание миграций

Базовая структура миграции

Каждая миграция — это PHP-класс с методами up() и down():


<?php

class Migration\_001\_Create\_Example\_Table {

public function up() {

global $wpdb;

$table\_name = $wpdb->prefix . 'example\_table';

$charset\_collate = $wpdb->get\_charset\_collate();

$sql = "CREATE TABLE IF NOT EXISTS $table\_name (

id bigint(20) NOT NULL AUTO\_INCREMENT,

name varchar(255) NOT NULL,

created\_at datetime DEFAULT CURRENT\_TIMESTAMP,

PRIMARY KEY (id)

) $charset\_collate;";

require\_once(ABSPATH . 'wp-admin/includes/upgrade.php');

dbDelta($sql);

}

public function down() {

global $wpdb;

$table\_name = $wpdb->prefix . 'example\_table';

$wpdb->query("DROP TABLE IF EXISTS $table\_name");

}

}

Enter fullscreen mode Exit fullscreen mode

Именование миграций

Правила:

  • Файл: 001_descriptive_name.php
  • Класс: Migration_001_Descriptive_Name

Имя класса формируется автоматически:

  • Префикс Migration_
  • Номер из имени файла
  • Каждое слово с заглавной буквы, разделено подчеркиванием

Примеры:

  • 001_create_users_table.php → Migration_001_Create_Users_Table
  • 002_add_status_column.php → Migration_002_Add_Status_Column
  • 003_migrate_old_data.php → Migration_003_Migrate_Old_Data

Примеры использования

Пример 1: Создание таблицы с проверкой


<?php

class Migration\_002\_Create\_Products\_Table {

public function up() {

global $wpdb;

$table\_name = $wpdb->prefix . 'products';

$charset\_collate = $wpdb->get\_charset\_collate();

$sql = "CREATE TABLE IF NOT EXISTS $table\_name (

id bigint(20) NOT NULL AUTO\_INCREMENT,

name varchar(255) NOT NULL,

price decimal(10,2) NOT NULL,

description text,

created\_at datetime DEFAULT CURRENT\_TIMESTAMP,

PRIMARY KEY (id),

KEY idx\_name (name)

) $charset\_collate;";

require\_once(ABSPATH . 'wp-admin/includes/upgrade.php');

dbDelta($sql);

}

public function down() {

global $wpdb;

$table\_name = $wpdb->prefix . 'products';

$wpdb->query("DROP TABLE IF EXISTS $table\_name");

}

}

Enter fullscreen mode Exit fullscreen mode

Пример 2: Добавление колонки с проверкой существования


<?php

class Migration\_003\_Add\_Status\_Column {

public function up() {

global $wpdb;

$table\_name = $wpdb->prefix . 'products';

// Проверяем существование колонки через Entity\_Migrator

if (!Entity\_Migrator::column\_exists($table\_name, 'status')) {

$table\_name\_escaped = esc\_sql($table\_name);

$wpdb->query("ALTER TABLE {$table\_name\_escaped} ADD COLUMN status varchar(50) DEFAULT 'active'");

}

}

public function down() {

global $wpdb;

$table\_name = $wpdb->prefix . 'products';

if (Entity\_Migrator::column\_exists($table\_name, 'status')) {

$table\_name\_escaped = esc\_sql($table\_name);

$wpdb->query("ALTER TABLE {$table\_name\_escaped} DROP COLUMN status");

}

}

}

Enter fullscreen mode Exit fullscreen mode

Пример 3: Безопасный перенос поста


<?php

class Migration\_004\_Import\_Post {

public function up() {

$post\_data = array(

'post\_title' => 'Название поста',

'post\_name' => 'post-slug',

'post\_content' => 'Содержимое поста',

'post\_status' => 'publish',

'post\_type' => 'post',

'meta' => array(

'custom\_field' => 'значение',

),

);

$result = Entity\_Migrator::migrate\_post($post\_data, array(

'update\_if\_exists' => true, // Обновлять если существует

'update\_meta' => true, // Обновлять мета-данные

'update\_terms' => true, // Обновлять термины

));

if (!$result['success']) {

throw new Exception('Ошибка импорта поста: ' . $result['error']);

}

}

public function down() {

// Откат: удаление поста

global $wpdb;

$post\_id = $wpdb->get\_var($wpdb->prepare(

"SELECT ID FROM {$wpdb->posts} WHERE post\_name = %s LIMIT 1",

'post-slug'

));

if ($post\_id) {

wp\_delete\_post($post\_id, true);

}

}

}

Enter fullscreen mode Exit fullscreen mode

Пример 4: Массовый импорт постов


<?php

class Migration\_005\_Import\_Multiple\_Posts {

public function up() {

// Получаем посты для импорта через Entity\_Selector

$posts = Entity\_Selector::get\_posts\_for\_migration(array(

'post\_type' => 'product',

'post\_status' => 'publish',

'numberposts' => 10,

));

foreach ($posts as $post\_data) {

// Убираем ID для безопасного импорта

unset($post\_data['ID']);

$result = Entity\_Migrator::migrate\_post($post\_data, array(

'update\_if\_exists' => true,

'update\_meta' => true,

'update\_terms' => true,

));

if (!$result['success']) {

error\_log('Ошибка импорта поста: ' . $result['error']);

}

}

}

public function down() {

// Откат: удаление импортированных постов

// Реализация зависит от логики

}

}

Enter fullscreen mode Exit fullscreen mode

Безопасный перенос сущностей

Entity_Migrator

Класс Entity_Migrator предоставляет методы для безопасного переноса сущностей WordPress:

migrate_post($post_data, $options)

Переносит или обновляет пост с проверкой существования:


$result = Entity\_Migrator::migrate\_post($post\_data, array(

'update\_if\_exists' => true, // Обновлять если существует

'skip\_if\_exists' => false, // Пропускать если существует

'update\_meta' => true, // Обновлять мета-данные

'update\_terms' => true, // Обновлять термины

));

// Результат:

// $result['success'] - успешность операции

// $result['action'] - 'created', 'updated' или 'skipped'

// $result['post\_id'] - ID поста

Enter fullscreen mode Exit fullscreen mode

migrate_user($user_data, $options)

Переносит или обновляет пользователя:


$result = Entity\_Migrator::migrate\_user($user\_data, array(

'update\_if\_exists' => true,

'update\_meta' => true,

));

Enter fullscreen mode Exit fullscreen mode

migrate_term($term_data, $options)

Переносит или обновляет термин:


$result = Entity\_Migrator::migrate\_term($term\_data, array(

'taxonomy' => 'category',

'update\_if\_exists' => true,

));

Enter fullscreen mode Exit fullscreen mode

Проверка существования

Плагин проверяет существование элементов по:

  • Для постов: ID, slug (post_name), мета-поля, заголовок + тип
  • Для пользователей: ID, логин, email
  • Для терминов: ID, slug, название + таксономия

Программный API

Запуск миграций


$runner = new Migration\_Runner();

// Запустить все невыполненные миграции

$results = $runner->run();

// Запустить конкретную миграцию

$results = $runner->run('001\_example\_migration');

Enter fullscreen mode Exit fullscreen mode

Откат миграций


// Откатить все миграции в обратном порядке

$results = $runner->rollback();

// Откатить конкретную миграцию

$results = $runner->rollback('001\_example\_migration');

Enter fullscreen mode Exit fullscreen mode

Проверка статуса


// Проверить, выполнена ли миграция

$is\_executed = Migrations\_Plugin::is\_migration\_executed('001\_example\_migration');

// Получить список выполненных миграций

$executed = Migrations\_Plugin::get\_executed\_migrations();

Enter fullscreen mode Exit fullscreen mode

Безопасность

Плагин следует стандартам безопасности WordPress:

  • Защита от SQL Injection — все запросы с переменными используют $wpdb->prepare() или esc_sql()
  • Защита от XSS — все выходные данные экранируются через esc_html(), esc_attr(), esc_url()
  • Защита от CSRF — проверка nonce для всех действий в админ-панели
  • Защита от Local File Inclusion — валидация путей к файлам миграций
  • Проверка прав доступа — current_user_can('manage_options') для всех административных функций
  • Валидация входных данных — санитизация всех пользовательских данных

Пример безопасного запроса


// ❌ Неправильно

$wpdb->query("SELECT \* FROM {$wpdb->posts} WHERE post\_title = '{$title}'");

// ✅ Правильно

$wpdb->get\_results($wpdb->prepare(

"SELECT \* FROM {$wpdb->posts} WHERE post\_title = %s",

$title

));

Enter fullscreen mode Exit fullscreen mode

Лучшие практики

  1. Всегда используйте dbDelta для создания таблиц

require\_once(ABSPATH . 'wp-admin/includes/upgrade.php');

dbDelta($sql);

Enter fullscreen mode Exit fullscreen mode
  1. Проверяйте существование перед изменением

if (!Entity\_Migrator::table\_exists($table\_name)) {

// Создаём таблицу

}

if (!Entity\_Migrator::column\_exists($table\_name, 'column\_name')) {

// Добавляем колонку

}

Enter fullscreen mode Exit fullscreen mode
  1. Используйте транзакции для критических операций

global $wpdb;

$wpdb->query('START TRANSACTION');

try {

// Выполняем операции

$wpdb->query('COMMIT');

} catch (Exception $e) {

$wpdb->query('ROLLBACK');

throw $e;

}

Enter fullscreen mode Exit fullscreen mode
  1. Всегда реализуйте метод down()

Каждая миграция должна иметь возможность отката:


public function down() {

// Откат изменений

}

Enter fullscreen mode Exit fullscreen mode
  1. Логируйте ошибки

if (!$result['success']) {

error\_log('Ошибка миграции: ' . $result['error']);

throw new Exception($result['error']);

}

Enter fullscreen mode Exit fullscreen mode

Структура плагина


migrations-plugin/

├── migrations/ # Папка с миграциями

│ ├── 001\_example\_migration.php

│ └── ...

├── includes/ # Основные классы

│ ├── class-migrations.php # Главный класс плагина

│ ├── class-migration-runner.php # Выполнение миграций

│ ├── class-entity-migrator.php # Перенос сущностей

│ ├── class-entity-selector.php # Выбор элементов

│ ├── class-export-loader.php # Загрузка экспортов

│ └── class-backup.php # Резервное копирование

├── admin/ # Админ-панель

│ └── class-migrations-admin.php # Интерфейс управления

├── migrations-plugin.php # Главный файл плагина

└── README.md # Документация

Enter fullscreen mode Exit fullscreen mode

Структура базы данных

Плагин создаёт таблицу wp_migrations_log для хранения истории выполненных миграций:

| Колонка | Тип | Описание |

| ---------------- | ------------- | ------------------------------- |

| id | bigint(20) | ID записи |

| migration_name | varchar(255) | Имя миграции |

| migration_file | varchar(255) | Имя файла миграции |

| executed_at | datetime | Дата и время выполнения |

| execution_time | decimal(10,4) | Время выполнения в секундах |

| status | varchar(20) | Статус (completed/failed) |

| error_message | text | Сообщение об ошибке (если есть) |


Итоги

Migrations Plugin — это мощный инструмент для управления изменениями базы данных WordPress. Он решает типичные задачи разработки:

  • ✅ Версионирование изменений БД
  • ✅ Автоматическое применение миграций
  • ✅ Безопасный перенос сущностей между сайтами
  • ✅ Откат изменений
  • ✅ Полное логирование операций
  • ✅ Удобный интерфейс управления

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


Примечание: Перед выполнением миграций на продакшн-сервере всегда делайте резервную копию базы данных!

Read more on viku-lov.ru

Top comments (0)