Migrate API і з чим його їдять. На прикладі міграції форуму з Drupal 7. Частина 1
Надіслано Animan в Пн, 2018/02/19 - 12:05
Я хотів би поділитись своїм досвідом міграції форуму з Drupal 7 на Drupal 8, з проблемами якими я стикався під час цього процесу і інструментами які я використовував. Також розповім за підводні каміння з якими я стикнувся при міграції форуму і термінів до нього.
Інструменти
Весь процес міграції ми будемо робити використовуючи Drush. Тепер визначимось з тим що нам необхідно для проведення міграції (я буду вказувати версії які я використовував на момент написання статті):
Drupal: 8.4.5
Модулі для міграції:
* migrate
* migrate_drupal
* migrate_plus: 8.x-4.0-beta2
* migrate_tools: 8.x-4.0-beta2
Drush: 9.1.0
Весь процес міграції відбувався з використанням:
PHP: 7.1.14
MySQL: MariaDB 10.1.31
Примітка:
Всі шляхи будуть вказані відносно кореня модуля (директорії custom_migration_forum, або так як ви назвете модуль).
Перед початком міграції вимкніть модуль rdf в Drupal 8, з ним можуть бути проблеми під час виконання Rolling back (коли скасовуємо зміни міграції).
Тестовий контент
Для актуальності інформації я вирішив по ходу написання статті дописувати модуль, який проводить міграцію форуму. Тестовий контент я створив використовуючи Devel для генерації контенту. Загальна кількість згенерованих тем форуму 300 та коментарі до них. Вийшло щось таке:
Підготовка до міграції
Спочатку ми розгорнемо чистий сайт на Drupal 8. На чистому сайті ми створюємо свій модуль, або використовуємо GitHub.
Створюємо файл custom_migration_forum.info.yml в який заносимо основну інформацію про наш модуль та залежності:
name: Custom Migration Forum description: Custom module for migrating forum from a Drupal 7 site. package: Migrations type: module core: 8.x dependencies: - drupal:migrate - drupal:migrate_drupal - drupal:forum - migrate_plus:migrate_plus (>=4.0-beta2) - migrate_tools:migrate_tools (>=4.0-beta2)
Для того щоб видалити старі конфіги міграцій при деінсталяції модуля це потрібно описати в custom_migration_forum.install. Бувало я стикався з ситуаціями коли виникав конфлікт конфігів, через те що старі конфіги не були видалені при деінсталяції модуля, тому щоб убезпечити себе краще видаляти їх при деінсталяції модуля.
custom_migration_forum.install
<?php /** * @file * Contains migrate_forum_drupal8.install. */ /** * Implements hook_uninstall(). * * Removes stale migration configs during uninstall. */ function custom_migration_forum_uninstall() { $query = \Drupal::database()->select('config', 'c'); $query->fields('c', ['name']); $query->condition('name', $query->escapeLike('migrate_plus.') . '%', 'LIKE'); $config_names = $query->execute()->fetchAll(); // Delete each config using configFactory. foreach ($config_names as $config_name) { \Drupal::configFactory()->getEditable($config_name->name)->delete(); } }
Або ви просто можете виставити залежність у кожної міграції:
dependencies: enforced: module: - custom_migration_forum
Міграція термінів форуму
Оскільки форум в Drupal 7 це по суті ноди з термінами, то для початку ми повинні мігрувати терміни. Почнемо з створення плагінів для міграції термінів і словника.
src/Plugin/migrate/source/Vocabulary.php:
<?php /** * @file * Contains \Drupal\migrate_therasomnia\Plugin\migrate\source\Vocabulary. */ namespace Drupal\custom_migration_forum\Plugin\migrate\source; use Drupal\migrate\Row; use Drupal\migrate\Plugin\migrate\source\SqlBase; /** * Drupal 7 vocabularies source from database. * * @MigrateSource( * id = "custom_migration_forum_vocabulary", * source_provider = "taxonomy" * ) */ class Vocabulary extends SqlBase { /** * {@inheritdoc} */ public function query() { $query = $this->select('taxonomy_vocabulary', 'v') 'vid', 'name', 'description', 'hierarchy', 'module', 'weight', 'machine_name' )); // Filtered out unnecessary dictionaries. $query->condition('machine_name', 'forums'); return $query; } /** * {@inheritdoc} */ public function fields() { 'vid' => $this->t('The vocabulary ID.'), 'name' => $this->t('The name of the vocabulary.'), 'description' => $this->t('The description of the vocabulary.'), 'help' => $this->t('Help text to display for the vocabulary.'), 'relations' => $this->t('Whether or not related terms are enabled within the vocabulary. (0 = disabled, 1 = enabled)'), 'hierarchy' => $this->t('The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)'), 'weight' => $this->t('The weight of the vocabulary in relation to other vocabularies.'), 'parents' => $this->t("The Drupal term IDs of the term's parents."), 'node_types' => $this->t('The names of the node types the vocabulary may be used with.'), ); } /** * {@inheritdoc} */ public function getIds() { $ids['vid']['type'] = 'integer'; return $ids; } }
src/Plugin/migrate/source/Terms.php:
<?php /** * @file * Contains \Drupal\migrate_therasomnia\Plugin\migrate\source\Terms. */ namespace Drupal\custom_migration_forum\Plugin\migrate\source; use Drupal\migrate\Row; use Drupal\migrate\Plugin\migrate\source\SqlBase; /** * Drupal 7 taxonomy terms source from database. * * @MigrateSource( * id = "custom_migration_forum_term", * source_provider = "taxonomy" * ) */ class Terms extends SqlBase { /** * {@inheritdoc} */ public function query() { $query = $this->select('taxonomy_term_data', 'td') ->fields('td', ['tid', 'vid', 'name', 'description', 'weight', 'format']) ->fields('tv', ['vid', 'machine_name']) ->distinct(); // Add table for condition on query. $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); // Filtered out unnecessary dictionaries. $query->condition('tv.machine_name', 'forums'); return $query; } /** * {@inheritdoc} */ public function fields() { return [ 'tid' => $this->t('The term ID.'), 'vid' => $this->t('Existing term VID'), 'name' => $this->t('The name of the term.'), 'description' => $this->t('The term description.'), 'weight' => $this->t('Weight'), 'parent' => $this->t("The Drupal term IDs of the term's parents."), ]; } /** * {@inheritdoc} */ public function prepareRow(Row $row) { // Find parents for this row. $parents = $this->select('taxonomy_term_hierarchy', 'th') ->fields('th', ['parent', 'tid']); $parents->condition('tid', $row->getSourceProperty('tid')); $parents = $parents->execute()->fetchCol(); return parent::prepareRow($row); } /** * {@inheritdoc} */ public function getIds() { $ids['tid']['type'] = 'integer'; return $ids; } }
Після створення плаґінів для міграції термінів та словників нам треба зробити конфігурацію для нашої міграції. Для цього створюємо migrate_plus.migration.term.yml і migrate_plus.migration.vocablary.yml.
config/install/migrate_plus.migration.vocablary.yml:
id: custom_migration_forum_vocabulary label: Taxonomy vocabulary forum migration_group: Custom Migration Forum dependencies: enforced: module: - custom_migration_forum source: plugin: custom_migration_forum_vocabulary target: migrate process: vid: - plugin: machine_name source: machine_name - plugin: make_unique_entity_field entity_type: taxonomy_vocabulary field: vid length: 32 migrated: true label: name name: name description: description hierarchy: hierarchy module: module weight: weight destination: plugin: entity:taxonomy_vocabulary
В 'source' ми вказуємо наш плаґін custom_migration_forum_vocabulary, який ми описали в класі Vocabulary.
config/install/migrate_plus.migration.term.yml:
id: custom_migration_forum_term label: Taxonomy terms forum migration_group: Custom Migration Forum dependencies: enforced: module: - custom_migration_forum source: plugin: custom_migration_forum_term target: migrate process: tid: tid vid: plugin: migration migration: custom_migration_forum_vocabulary source: vid name: name description: description weight: weight parent: parent changed: timestamp destination: plugin: entity:taxonomy_term migration_dependencies: required: - custom_migration_forum_vocabulary
При міграції термінів ми ставимо в vid вказуємо машинне ім'я (custom_migration_forum_vocabulary) з якої ми будемо брати ID словника форуму.
Міграція форуму
Хотів би звернути увагу що в даній статті ми мігруємо форум без додаткових полів. Перед тим як почати міграцію форумів, нам потрібно мігрувати користувачів, бо без них в форумів не буде авторів. Її ми проведемо використовуючи плаґіни з Ядра, але додамо їх в свою групу міграції "Custom Migration Forum", щоб можна було запустити всю міграцію однією командою. Нам потрібні 3 міграції:
- міграція ролей (custom_migration_forum_user_role)
- міграція користувачів (custom_migration_forum_user)
- міграція форматів тексту (custom_migration_forum_filter_format).
Ось наші три yml-файли.
config/install/migrate_plus.migration.user_role.yml:
id: custom_migration_forum_user_role label: User roles migration_group: Custom Migration Forum dependencies: enforced: module: - custom_migration_forum source: plugin: d7_user_role process: id: - plugin: machine_name source: name - plugin: user_update_8002 label: name permissions: - plugin: static_map source: permissions bypass: true map: 'use PHP for block visibility': 'use PHP for settings' 'administer site-wide contact form': 'administer contact forms' 'post comments without approval': 'skip comment approval' 'edit own blog entries': 'edit own blog content' 'edit any blog entry': 'edit any blog content' 'delete own blog entries': 'delete own blog content' 'delete any blog entry': 'delete any blog content' 'create forum topics': 'create forum content' 'delete any forum topic': 'delete any forum content' 'delete own forum topics': 'delete own forum content' 'edit any forum topic': 'edit any forum content' 'edit own forum topics': 'edit own forum content' - plugin: flatten weight: weight destination: plugin: entity:user_role migration_dependencies: optional: - custom_migration_forum_filter_format
config/install/migrate_plus.migration.user.yml:
id: custom_migration_forum_user label: User accounts migration_group: Custom Migration Forum dependencies: enforced: module: - custom_migration_forum class: Drupal\user\Plugin\migrate\User source: plugin: d7_user process: uid: uid name: name pass: pass mail: mail created: created access: access login: login status: status timezone: timezone langcode: plugin: user_langcode source: language fallback_to_site_default: false preferred_langcode: plugin: user_langcode source: language fallback_to_site_default: true preferred_admin_langcode: plugin: user_langcode source: language fallback_to_site_default: true init: init roles: plugin: migration_lookup migration: custom_migration_forum_user_role source: roles user_picture: - plugin: default_value source: picture default_value: null - plugin: migration_lookup migration: d7_file destination: plugin: entity:user migration_dependencies: required: - custom_migration_forum_user_role optional: - d7_field_instance - d7_file - language - default_language - user_picture_field_instance - user_picture_entity_display - user_picture_entity_form_display
config/install/migrate_plus.migration.filter_format.yml:
id: custom_migration_forum_filter_format label: Filter format configuration migration_group: Custom Migration Forum dependencies: enforced: module: - custom_migration_forum source: plugin: d7_filter_format process: format: format name: name cache: cache weight: weight filters: plugin: sub_process source: filters key: '@id' process: id: plugin: filter_id bypass: true source: name map: { } settings: plugin: filter_settings source: settings status: plugin: default_value default_value: true weight: weight destination: plugin: entity:filter_format
А тепер можемо почати підготовувати плаґін для міграції матеріалів форуму.
src/Plugin/migrate/source/Forum.php:
<?php namespace Drupal\custom_migration_forum\Plugin\migrate\source; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\migrate\Row; use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleHandler; use Drupal\Core\State\StateInterface; use Drupal\migrate\Plugin\MigrationInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Extract forum from Drupal 7 database. * * @MigrateSource( * id = "custom_migration_forum_forum", * ) */ class Forum extends FieldableEntity { /** * The module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ protected $moduleHandler; /** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler) { parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_manager); $this->moduleHandler = $module_handler; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { return new static( $configuration, $plugin_id, $plugin_definition, $migration, $container->get('state'), $container->get('entity.manager'), $container->get('module_handler') ); } /** * The join options between the node and the node_revisions table. */ /** * {@inheritdoc} */ public function query() { // Select node in its last revision. $query = $this->select('node_revision', 'nr') ->fields('n', [ 'nid', 'type', 'language', 'status', 'created', 'changed', 'comment', 'promote', 'sticky', 'tnid', 'translate', ]) ->fields('nr', [ 'vid', 'title', 'log', 'timestamp', ]) ->fields('fb', [ 'body_value', 'body_format', ]); $query->addField('n', 'uid', 'node_uid'); $query->addField('n', 'type', 'node_type'); $query->addField('nr', 'uid', 'revision_uid'); $query->innerJoin('field_data_body', 'fb', 'n.nid = fb.entity_id'); // If the content_translation module is enabled, get the source langcode // to fill the content_translation_source field. if ($this->moduleHandler->moduleExists('content_translation')) { $query->leftJoin('node', 'nt', 'n.tnid = nt.nid'); $query->addField('nt', 'language', 'source_langcode'); } $this->handleTranslations($query); // Filtered node type forum. $query->condition('n.type', 'forum'); return $query; } /** * {@inheritdoc} */ public function prepareRow(Row $row) { // Get Field API field values. $nid = $row->getSourceProperty('nid'); $vid = $row->getSourceProperty('vid'); $row->setSourceProperty($field, $this->getFieldValues('node', $field, $nid, $vid)); } // Make sure we always have a translation set. if ($row->getSourceProperty('tnid') == 0) { $row->setSourceProperty('tnid', $row->getSourceProperty('nid')); } return parent::prepareRow($row); } /** * {@inheritdoc} */ public function fields() { $fields = [ 'nid' => $this->t('Node ID'), 'type' => $this->t('Type'), 'title' => $this->t('Title'), 'body_value' => $this->t('Full text of body'), 'body_format' => $this->t('Format of body'), 'node_uid' => $this->t('Node authored by (uid)'), 'revision_uid' => $this->t('Revision authored by (uid)'), 'created' => $this->t('Created timestamp'), 'changed' => $this->t('Modified timestamp'), 'status' => $this->t('Published'), 'promote' => $this->t('Promoted to front page'), 'sticky' => $this->t('Sticky at top of lists'), 'revision' => $this->t('Create new revision'), 'language' => $this->t('Language (fr, en, ...)'), 'tnid' => $this->t('The translation set id for this node'), 'timestamp' => $this->t('The timestamp the latest revision of this node was created.'), ]; return $fields; } /** * {@inheritdoc} */ public function getIds() { $ids['nid']['type'] = 'integer'; $ids['nid']['alias'] = 'n'; return $ids; } /** * Adapt our query for translations. * * @param \Drupal\Core\Database\Query\SelectInterface $query * The generated query. */ protected function handleTranslations(SelectInterface $query) { // Check whether or not we want translations. // No translations: Yield untranslated nodes, or default translations. $query->where('n.tnid = 0 OR n.tnid = n.nid'); } else { // Translations: Yield only non-default translations. $query->where('n.tnid <> 0 AND n.tnid <> n.nid'); } } }
І yml-файл до плаґіну.
config/install/migrate_plus.migration.forum.yml:
id: custom_migration_forum_forum label: Custom forum migration migration_group: Custom Migration Forum dependencies: enforced: module: - custom_migration_forum source: plugin: custom_migration_forum_forum node_type: forum target: migrate migration_dependencies: required: - custom_migration_forum_term - custom_migration_forum_user - custom_migration_forum_filter_format process: nid: tnid vid: vid langcode: plugin: default_value source: language default_value: 'en' title: title type: plugin: default_value default_value: forum 'body/value': body_value 'body/format': body_format uid: plugin: migration migration: custom_migration_forum_user source: node_uid status: status created: created changed: changed promote: promote sticky: sticky revision_uid: revision_uid revision_log: log revision_timestamp: timestamp taxonomy_forums: plugin: migration migration: custom_migration_forum_term source: taxonomy_forums destination: plugin: entity:node
Останнє що залишилось, це коментарі до тем, а як же без них ? :)
Створюємо плаґін і описуємо його в конфігурації.
src/Plugin/migrate/source/ForumComment.php:
<?php /** * @file * Contains \Drupal\custom_migration_forum\Plugin\migrate\source\ForumComment. */ namespace Drupal\custom_migration_forum\Plugin\migrate\source; use Drupal\migrate\Row; use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity; /** * Drupal 7 comment forum source from database. * * @MigrateSource( * id = "custom_migration_forum_forum_comment", * source_provider = "comment", * ) */ class ForumComment extends FieldableEntity { /** * {@inheritdoc} */ public function query() { $query = $this->select('comment', 'c')->fields('c'); $query->innerJoin('node', 'n', 'c.nid = n.nid'); $query->addField('n', 'type', 'node_type'); $query->addField('n', 'nid'); $query->condition('n.type', 'forum'); $query->orderBy('c.created'); return $query; } /** * {@inheritdoc} */ public function prepareRow(Row $row) { $cid = $row->getSourceProperty('cid'); $node_type = $row->getSourceProperty('node_type'); $comment_type = 'comment_node_' . $node_type; $row->setSourceProperty('comment_type', 'comment_forum'); $row->setSourceProperty($field, $this->getFieldValues('comment', $field, $cid)); } return parent::prepareRow($row); } /** * {@inheritdoc} */ public function fields() { return [ 'cid' => $this->t('Comment ID.'), 'pid' => $this->t('Parent comment ID. If set to 0, this comment is not a reply to an existing comment.'), 'nid' => $this->t('The {node}.nid to which this comment is a reply.'), 'uid' => $this->t('The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.'), 'subject' => $this->t('The comment title.'), 'comment' => $this->t('The comment body.'), 'hostname' => $this->t("The author's host name."), 'created' => $this->t('The time that the comment was created, as a Unix timestamp.'), 'changed' => $this->t('The time that the comment was edited by its author, as a Unix timestamp.'), 'status' => $this->t('The published status of a comment. (0 = Published, 1 = Not Published)'), 'format' => $this->t('The {filter_formats}.format of the comment body.'), 'thread' => $this->t("The vancode representation of the comment's place in a thread."), 'name' => $this->t("The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form."), 'mail' => $this->t("The comment author's email address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on."), 'homepage' => $this->t("The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on."), 'type' => $this->t("The {node}.type to which this comment is a reply."), ]; } /** * {@inheritdoc} */ public function getIds() { $ids['cid']['type'] = 'integer'; return $ids; } }
config/install/migrate_plus.migration.forum_comment.yml:
id: custom_migration_forum_forum_comment label: Comments forum migration_group: Custom Migration Forum dependencies: enforced: module: - custom_migration_forum source: plugin: custom_migration_forum_forum_comment target: migrate constants: entity_type: node process: cid: cid pid: plugin: migration_lookup migration: custom_migration_forum_forum_comment source: pid entity_id: nid entity_type: 'constants/entity_type' comment_type: comment_type field_name: comment_type subject: subject uid: uid name: name mail: mail homepage: homepage hostname: hostname created: created changed: changed status: status thread: thread comment_body: comment_body destination: plugin: entity:comment migration_dependencies: required: - custom_migration_forum_forum
Початок міграції
Тепер ми готові для проведення міграції. Додамо базу даних Drupal 7 в файл конфігурації нашого сайту на Drupal 8. Робимо це в файлі settings.php (або іншому який піключає ваші налаштування, в мене це settings.local.php):
'database' => 'Drupal_7', 'username' => 'root', 'password' => 'root', 'prefix' => '', 'host' => 'localhost', 'port' => '3306', 'namespace' => 'Drupal\Core\Database\Driver\mysql', 'driver' => 'mysql', );
Вмикаємо наступні модулі:
drush en migrate migrate_drupal migrate_plus migrate_tools taxonomy forum -y
або якщо ви завантажили модуль з GitHub, то можете просто увімкнути його:
drush en custom_migration_forum -y
Міграція через Drush
Перевіряємо список доступних міграцій:
drush ms
Запускаємо всі наші міграції однією командою.
drush mim --group="Custom Migration Forum"
Для того щоб відмінити зміни міграції існує команда drush mr:
drush mr --group="Custom Migration Forum"
Також інколи виникають помилки при міграції і міграція зависає в статусі Importing або Rolling back, щоб скинути статус міграції потрібно запустити:
drush php-eval 'var_dump(Drupal::keyValue("migrate_status")->set('custom_migration_forum_forum', 0))'
Де custom_migration_forum_forum це ID міграції.
Наша міграція форуму завершена і як результат ми маємо повністю мігрований форум з користувачами і коментарями до тем.
Останні коментарі