Миграция контента из Drupal 6 в 7 при помощи Migrate API
Привет, друпалеры! С трудом нашел несколько часов, чтобы дописать этот пост: прям разрываюсь между Docker'ом, Drupal 8 и этой статьей. Но, если не написать сейчас, то велик шанс того, что материал так и не дойдет до блога. Рассказывать я сегодня буду о миграции данных с Drupal 6 на 7, используя модуль Migrate.
Как и многие записи в блоге, этот материал стал результатом моего знакомства с новым для меня модулем — Migrate. Да, разрабатывая уже более 5 лет на Drupal, я впервые столкнулся с задачей на перенос сайта с 6-ой версии Drupal'a на 7-ую — чему был несказанно рад. Ну а как не радоваться, когда ты изучаешь что-то новое для себя, а тебе за конечный результат еще и деньги платят?
Для начала, как обычно, немного о проекте, в рамках которого осуществлялась миграция данных. Задачей проекта не являлось полностью скопировать сайт с 6-ки на 7-ку — необходимо было внести значительное число правок в структуру контента, а именно:
- перенести на новый сайт лишь ноды определнных контент типов (Content types);
- несколько контент типов должны быть перенесены в один;
- уменьшение количества терминов таксономии: заказчик предоставил документ в котором указаывалось на какой термин необходимо заменить термины со старого сайта;
- добавление новых полей, значения которых должны будут формироваться на основе данных со старого сайта;
- на новый сайт должны быть перенесены лишь необходимые файлы, которые связаны с полями нод;
- сохранение связей Entity reference между нодами (была связка из трех контент типов: "Конференция" - "Выступление" - "Докладчик");
- полное изменение дизайна сайта.
В общем, миграция данных была самым интересным таском на проекте и я забрал его конечно же себе. Реализовывать перенос контента с Drupal 6 на 7 было решено с использованием модуля Migrate. Собственно, что же этот модуль позволяет и нужен ли он вообще?
Migrate API
Модуль Migrate — это своего рода фреймворк для переноса данных с различных источников в Drupal. Т.е. перенос сайта с Drupal 6 на 7 — это частный случай. С таким же успехом вы можете импортировать данные с XML, JSON источников, а также с баз данных других фреймворков — например с Wordpress и Joomla. Для полноценной работы Migrate необходимо наличие уникальных ключей для поставляемых данных. От части Migrate является более гибким решением по сравнению с модулем Feeds.
Как вы должны понимать, для начала продуктивной работы с каким бы то ни было фреймворком вам необходимо знать его API. Лично у меня ушло порядка 2 дней для того, чтобы более менее ориентироваться в Migrate API. Да, сразу может показаться, что вы зря теряете время на изучение какого-то инструмента, но, уверяю вас, в будущем это время окупится вам сполна!
Пожалуй, приведу несколько аргументов для того, чтобы окончательно вас убедить в том, что Migrate — полезная штука:
- возможность настраивать миграцию как через собственный модуль, так и используя интерфейс в админке;
- возможность отката (Rollback) миграции, если что-то пошло не так (ну или в целях тестирования);
- возможность поэтапной миграции данных;
- достаточно гибкое API (я нашел в нем отклик на все мои нестандартные "хотелки");
- возможность миграции и импорта данных с БД, XML, RSS, CSV;
- готовые модули для стандартных миграций с D6, Wordperss.
Модуль Migrate включает в себя аж 2 модуля примеров 'migrate_example' и 'migrate_example_baseball' — именно с разбора кода этих модулей и стоит начинать, т.к. он обильно покрыт комментариями, проливающими свет на API. Так-с.. модули посмотреть можете и попозже, сначала мой пост дочитайте: я ж тут все по-русски разжевывать буду!
Миграция контента Drupal-to-Drupal
Как я уже сказал, Migrate — универсальный инструмент для миграции данных с различных источников. Однако для наиболее популярных задач уже существуют дополнительные модули: например, для миграции Drupal-to-Drupal или же Wordpress-to-Drupal. Не стоит пренебрегать ими — ставьте сразу. Собственно, как же работают эти sub-модули и в целом сам Migrate?
Фреймворк Migrate, в отличие от нынешнего Drupal 7, написан с использованием ООП, а это значит, что вам необходимо понимать несколько вещей: что такое "класс", "метод" и "наследование". Таким образом, модуль предоставляют набор готовых классов, призванных упростить миграцию данных. Sub-модули предоставляют дополнительные классы, которые унаследованы от базовых модуля Migrate и имеют более функциональные методы под конкретную задачу.
Что такое Source & Destination?
Чисто интуитивно уже можно догадаться: Source — это источник с данными, Destination — это ваша база данных, куда необходимо перетянуть данные. Эти понятия — можно сказать, основа идеологии Migrate. Ваша задача, как раз и состоит, в том, чтобы настроить правила миграции ("маппинг" ин инглиш) из Source в Destination. Migrate позаботился даже о программистах-кликерах: базовый маппинг вы можете настроить через админку. Однако админка — это лишь вершина айсберга по сравнению с тем, что можно вытворять в собственных классах.
Раз я рассказываю про миграцию Drupal-to-Drupal, то пора бы уже пролить свет и на то, как же подключиться в базе данных D6. У вас есть два варианта: вы можете поднять локально дамп БД или же подключаться прямо к продакшену. Настройки подключения к базе данных D6 рекомендуется внести прямо в settings.php — просто добавьте в массив
еще один массив, например, с ключом $database
. Далее вам придется указывать этот самый ключ 'legacy'
в качестве значения для 'legacy'
(это приблуда от модуля migrate_d2d).'source_connection'
Вот. Теперь, будем, считать ваш Drupal знает и про Source, и про Destination базы данных. Самое время создать модуль и настроить маппинг.
Создание модуля миграции на базе Migrate API
Итак, оставим килознаки теории и перейдем к практике. Модуль, с которого будут приведены примеры, у меня, если что, называется 'ncsrc_migration'. Info-файл модуля ничего необычного не содержит, кроме того, что необходимо подключать файлы с классами миграции:
- name = NCSRC Migration
- description = Migration of content from old site.
- package = NCSRC
- core = 7.x
-
- dependencies[] = migrate (>=7.x-2.7)
- dependencies[] = migrate_d2d (>=7.x-2.1)
-
- # Это файл с общими и абстрактными классами.
- files[] = includes/ncsrc_migration.general.inc
- # Класс для миграции материалов типа "Докладчик".
- files[] = includes/ncsrc_migration.presenter_ct.inc
- # Класс для миграции материалов типа "Выступление".
- files[] = includes/ncsrc_migration.session_ct.inc
- # Класс для миграции материалов типа "Конференция".
- files[] = includes/ncsrc_migration.event_ct.inc
- # Класс для миграции материалов типа "Публикация".
- files[] = includes/ncsrc_migration.publication_ct.inc
- # Класс для миграции материалов типа "Рассылка",
- # которые на новый сайт будут перенесены как "Публикация".
- files[] = includes/ncsrc_migration.newsletter_ct.inc
Основной файл модуля 'ncsrc_migration.module' у меня так и остался пустым. Имплементацию хука hook_migrate_api, согласно канонам, лучше закинуть в файл 'ncsrc_migration.migrate.inc':
- /**
- * Implements hook_migrate_api().
- */
- function ncsrc_migration_migrate_api() {
- /**
- * Declare the api version and migration group.
- */
- $api = array(
- 'api' => 2,
- // Тут описываем группы миграции. Например, можно объединить в одну
- // группу миграции, связанные с конференциями, выступлениями и докладчиками.
- 'groups' => array(
- 'event_ct' => array(
- 'title' => t('Content type: Event, Session'),
- ),
- 'publication_ct' => array(
- 'title' => t('Content type: Publication'),
- ),
- ),
- // Описание классов миграции. Напомнимаю, что все классы будут унаследованы
- // от расширенных классов, описанных в 'migrate_d2d' модуле. Поэтому достпуны
- // некоторые доп. опции такие, как 'source_type' и 'destination_type'.
- 'migrations' => array(
- 'PresenterNode' => array(
- 'class_name' => 'NcSrcPresenterNodeMigration',
- 'group_name' => 'event_ct',
- 'description' => t('Migration of nodes of Presenter content type.'),
- // Машинное имя Content type на Drupal 6 сайте.
- 'source_type' => 'presenter',
- // Машинное имя Content type на Drupal 7 сайте.
- 'destination_type' => 'presenter',
- ),
- 'SessionNode' => array(
- 'class_name' => 'NcSrcSessionNodeMigration',
- 'group_name' => 'event_ct',
- 'description' => t('Migration of nodes of Session content type.'),
- 'source_type' => 'agenda',
- 'destination_type' => 'session',
- // Вот тут указываем, что миграция нод типа "Выступление"
- // зависит от миграции нод типа "Докладчик". Другими словами,
- // пока не перетянете всех докладчиков Migrate не даст тянуть выступления.
- 'dependencies' => array(
- 'PresenterNode',
- ),
- ),
- 'EventNode' => array(
- 'class_name' => 'NcSrcEventNodeMigration',
- 'group_name' => 'event_ct',
- 'description' => t('Migration of nodes of Event content type.'),
- 'source_type' => 'event',
- 'destination_type' => 'event',
- 'dependencies' => array(
- 'SessionNode',
- ),
- ),
- 'PublicationNode' => array(
- 'class_name' => 'NcSrcPublicationNodeMigration',
- 'group_name' => 'publication_ct',
- 'description' => t('Migration of nodes of Publication content type.'),
- 'source_type' => 'resource',
- 'destination_type' => 'publication',
- ),
- 'NewsletterNode' => array(
- 'class_name' => 'NcSrcNewsletterNodeMigration',
- 'group_name' => 'publication_ct',
- 'description' => t('Migration of nodes of Newsletter content type.'),
- 'source_type' => 'newsletter',
- // Вот тут можете обратить внимание, что миграция,
- // как и в предыдущем случае будет осуществляеться в один и тот же контент тип.
- 'destination_type' => 'publication',
- ),
- ),
- );
-
- return $api;
- }
На самом деле имплементация хука hook_migrate_api — это самое простое. Теперь необходимо описать все указанные классы, да при этом еще соблюдая все требования Migrate API. Начну я пожалуй со вспомогательного файла 'ncsrc_migration.general.inc':
- // Вот он базовый класс для всех остальных моих классов миграции.
- // Как видите, идет наследование от DrupalNode6Migration - именно этот класс и
- // предоставляет модуль 'migrate_d2d'.
- abstract class NcSrcMigration extends DrupalNode6Migration {
- // Всякие необходимые константы. Например, мне нужны были ID словарей таксономии.
- const NCSRC_MIGRATION_SOURCE_FOCUS_AREAS_VID = '999';
-
- // Кастомные свойства класса.
- protected $ncsrc_source_site;
- protected $ncsrc_timezone;
- protected $ncsrc_text_format = 'filtered_html';
-
- // Конструктор для определение свойств класса.
- public function __construct(array $arguments) {
- // Site's default timezone.
- $this->ncsrc_timezone = variable_get('date_default_timezone', '');
-
- // Прочий код..
- // Кстати, аргументы 'source_version' и 'source_connection' обязательны.
- // Без них класс DrupalNode6Migration будет жутко ругаться.
- // Однако, из можно указать и в hook_migrate_api, если не ошибаюсь.
-
- // Version of old site's Drupal.
- $arguments['source_version'] = '6';
-
- // Database connection. See settings.php
- $arguments['source_connection'] = $this->ncsrc_source_db;
-
- parent::__construct($arguments);
- }
-
- // Далее идет серия методов для обработки терминов таксономии.
- // Например, на старом сайте было 3 термина А, Б, В.
- // На новом сайте должен остаться только термин Б. Причем все ноды,
- // имеющие связи с А и В, должны переключиться на Б.
- // Это как пример того, что позволяет делать Migrate.
- protected function taxonomyReTagging(&$row, $vids) { ... }
- }
-
- // В рамках проекта пришлось переопределять класс MigrateFileUri,
- // т.к. он не позволял забирать файлы с сайта, который находился
- // на серваке с HTTP авторизацией. Т.е. URL'ы вида
- // http://user:pass@domain.com/path_to_file обрабатывались некорректно.
- // Опять же приведено, как пример гибкости Migrate.
- class NcSrcMigrateFileUri extends MigrateFileUri {
- static public function urlencode($filename) { ... }
- protected function copyFile($destination) { ... }
- }
Понимаю, что с первого взгляда нихрена не понятно, но я не буду останавливаться и продолжу валить листингами кода. Читайте комментарии к коду. Начнем с класса миграции типа контента Докладчик (он же Presenter):
- // Данный класс наследует вышеописанный базовый класс, как я и обещал.
- class NcSrcPresenterNodeMigration extends NcSrcMigration {
-
- public function __construct(array $arguments) {
- parent::__construct($arguments);
-
- // Image field.
- // @see beer.inc in migrate_example module.
- $this->addFieldMapping('field_presenter_image', 'filepath');
- $this->addFieldMapping('field_presenter_image:file_class')
- ->defaultValue('NcSrcMigrateFileUri');
- $this->addFieldMapping('field_presenter_image:source_dir')
- ->defaultValue($this->ncsrc_source_site);
-
- $this->addUnmigratedDestinations(array(
- 'field_presenter_image:language',
- 'field_presenter_image:preserve_files',
- 'field_presenter_image:destination_dir',
- 'field_presenter_image:destination_file',
- 'field_presenter_image:file_replace',
- 'field_presenter_image:urlencode',
- 'field_presenter_image:alt',
- 'field_presenter_image:title'
- ));
-
- // Job Title field.
- $this->addFieldMapping('field_presenter_job', 'field_job_title_presenter');
- $this->addUnmigratedDestinations(array('field_presenter_job:language'));
-
- // Organization Link field.
- $this->addFieldMapping('field_presenter_org_link', 'field_organization_presenter');
- $this->addFieldMapping('field_presenter_org_link:title', 'field_organization_presenter:title');
- $this->addUnmigratedDestinations(
- array('field_presenter_org_link:attributes', 'field_presenter_org_link:language'));
-
- // Non-migrated Sources.
- $this->addUnmigratedSources(array(
- 'uid',
- // ...
- 'totalcount',
- ));
-
- // Removes mappings to prevent warning messages.
- $this->removeFieldMapping('body:language');
- $this->removeFieldMapping('pathauto');
- }
-
- protected function query() {
- $query = parent::query();
-
- // Updates query for image migration.
- // @todo: will be broken with multiple values.
- // Короче так делать плохо, ибо если у поля несколько значений, то будут дубли
- // строк в результатх запроса и как следствие в Destination базе окажется
- // лишь последнее значение вместо всех. Для single значений в принципе прокатит.
- $query->leftJoin('files', 'files', 'f.field_presenter_img_fid = files.fid');
- $query->fields('files', array('filepath'));
-
- return $query;
- }
-
- function prepare(&$row) {
- // Required for disabling alias generating by Pathauto module.
- $row->path['pathauto'] = 0;
- }
- }
Видимо, все же придется остановиться и прояснить некоторый моменты. По сути ваш класс миграции — это не только маппинг полей, но и возможность адаптировать миграцию под вашу конкретную задачу. Каждый метод в этом примере расширяет функционал метода из родительского класса, который предоставляет Migrate. Если говорить языком Drupal'a, то представьте, что эти методы — это хуки, которые мы привыкли имплементировать. Попробую немного объяснить какой метод за что отвечает и что в него надо писать.
Основные методы классов Migrate
__construct() — конструктор класса, стреляет сразу же, когда дело доходит до инициализации вашего класса. Не забывайте включать
в начало вашего кода, иначе рискуете потерять важные данные из родительских классов. В этом методе настраивается, как правило, маппинг полей. Откуда я взял все эти ключи полей? Принцип довольно прост: открываете в админке Task (термин взял как раз из интерфейса) с этой миграцией и глядите на что ругается вам система: какие Sorce поля не описаны, какие Destination поля остались неиспользуемыми. Собственно ваша задача настроить маппинг так, чтобы в интерфейсе не было никаких красных шрифтов с ошибками и предупреждениями. Для этого у вас есть набор методов, таких как parent::__construct($arguments);
, addFieldMapping()
, addUnmigratedDestinations()
.removeFieldMapping()
query() — это ваша возможность "альтернуть" запрос, которым будут выгребаться данные из Source таблицы. Результат конечного запроса вы кстати опять же можете глянуть в интерфейсе на вкладке Source. В общем, этот метод нужен для того, чтобы вытягивать из Source базы больше данных, чем это делают родительские классы. На самом деле, если не включать
, то вы можете написать свой query с нуля сами.$query = parent::query();
prepareRow() — отрабатывает после query(), позволяет изменить Source данные, которые придут на маппинг. Если у вас не получается одним запросом выгрести все данные из Source базы, то в этом методе можете инициировать еще несколько дополнительных запросов. Конечно лучше поработать над основным запросом в query(), но не всегда это удается. Чуть ниже вы увидите пример с этим методом.
prepare() — стреляет перед сохранением объекта в Destination базу. Это один из последних этапов миграции: данные из Source вытянуты, преобразованы согласно маппингу и готовы к сохранению. В вышеприведенном коде мне надо было отключить генерацию алиаса, чтобы использовался алиас с Sorce сайта.
complete() — стреляет после того, как объект сущности уже сохранен в Destination. Я был очень удивлен, когда уперся в его необходимость и нашел его в родительских классах. Все же приятно, когда ты только захотел, а оно уже и есть. Пример использования увидите ниже в миграции типа Webinar.
Методы разместил в порядке их выполнения. Теперь опять перейдем к наглядным примерам. Класс для миграции ноды типа "Доклад" (Session в оригинале):
- class NcSrcSessionNodeMigration extends NcSrcMigration {
-
- public function __construct(array $arguments) {
- parent::__construct($arguments);
-
- // Attachment field.
- $this->addFieldMapping('field_session_attachment', 'field_downloads');
- $this->addFieldMapping('field_session_attachment:file_class')
- ->defaultValue('NcSrcMigrateFileUri');
- $this->addFieldMapping('field_session_attachment:source_dir')
- ->defaultValue($this->ncsrc_source_site);
-
- $this->addUnmigratedDestinations(array(
- 'field_session_attachment:language',
- // ...
- 'field_session_attachment:display',
- ));
-
- // Date Published field.
- $this->addFieldMapping('field_session_date', 'field_agenda_date');
- $this->addFieldMapping('field_session_date:to', 'field_agenda_date:value2');
- $this->addFieldMapping('field_session_date:timezone')
- ->defaultValue($this->ncsrc_timezone);
- $this->addUnmigratedDestinations(array(
- 'field_session_date:rrule',
- ));
-
- // Session Location field.
- $this->addFieldMapping('field_session_location', 'field_location');
- $this->addFieldMapping('field_session_location:format')
- ->defaultValue($this->ncsrc_text_format);;
- $this->addUnmigratedDestinations(array(
- 'field_session_location:language',
- ));
-
- // Speaker field (reference to Presenter).
- // А вот так легко можно свзять одну ноду с другой через Entity Reference
- // поле, например. Напоминаю, что Presenter мигрируется до этого типа
- // контента.
- $this->addFieldMapping('field_session_speaker', 'field_presenter_references')
- ->sourceMigration('PresenterNode');
-
- // Non-migrated Destinations.
- $this->addUnmigratedDestinations(array(
- 'field_session_day',
- // ...
- 'field_session_video:display',
- ));
-
- // Non-migrated Sources.
- $this->addUnmigratedSources(array(
- 'uid',
- // ...
- 'totalcount',
- ));
-
- // Removes mappings to prevent warning messages.
- $this->removeFieldMapping('pathauto');
- }
-
- public function prepare(&$row) {
- // Required for disabling alias generating by Pathauto module.
- $row->path['pathauto'] = 0;
-
- // Изменяем текстовый формат - хороший пример того, чем еще может быть
- // полезен данный метод в процессе миграции.
- $row->body[LANGUAGE_NONE][0]['value_format'] = $this->ncsrc_text_format;
- $row->body[LANGUAGE_NONE][0]['format'] = $this->ncsrc_text_format;
-
- // Тут был какой-то головняк с миграцией дат, пришлось изобретать.
- if (isset($row->field_session_date[LANGUAGE_NONE][0])) {
- $value = $row->field_session_date[LANGUAGE_NONE][0]['value'];
-
- $timezone = new DateTimeZone($this->ncsrc_timezone);
- $date = new DateObject($value, $timezone);
- $offset = $timezone->getOffset($date);
-
- $row->field_session_date[LANGUAGE_NONE][0]['value'] += $offset;
- $row->field_session_date[LANGUAGE_NONE][0]['value2'] += $offset;
- }
- }
-
- public function prepareRow($current_row) {
- // Always start your prepareRow implementation with this clause. You need to
- // be sure your parent classes have their chance at the row, and that if
- // they return FALSE (indicating the row should be skipped) you pass that
- // on.
- // Короче это важная штука, ее надо ОБЯЗАТЕЛЬНО вставлять,
- // если определяете данный метод в своем классе.
- if (parent::prepareRow($current_row) === FALSE) {
- return FALSE;
- }
-
- // Переключаемся на Source БД.
- db_set_active($this->ncsrc_source_db);
-
- // Собственно, вот он рабочий вариант для миграции файловых
- // полей с несколькими значенияи (multi value).
- $query = db_select('content_field_downloads', 'fd');
- $query->leftJoin('files', 'f', 'fd.field_downloads_fid = f.fid');
- $query->fields('f', array('filepath'));
- $query->condition('fd.vid', $current_row->vid);
- $files_result = $query->execute();
-
- // Опять тут с датами какая-то алхимия.
- $query = db_select('content_field_agenda_reference', 'far');
- $query->leftJoin('content_field_event_date', 'fed', 'fed.vid = far.vid');
- $query->fields('fed', array('field_event_date_value'));
- $query->condition('far.field_agenda_reference_nid', $current_row->nid);
- $query->orderBy('far.nid', 'ASC');
- $query->range(0, 1);
- $date_result = $query->execute()->fetchField();
-
- db_set_active();
-
- $current_row->field_downloads = array();
- foreach ($files_result as $row) {
- $current_row->field_downloads[] = $row->filepath;
- }
-
- $current_row->field_event_date_value = $date_result;
-
- return TRUE;
- }
- }
Заключительный листинг миграции контент типа Event для связки "Конференция" - "Выступление" - "Докладчик":
- class NcSrcEventNodeMigration extends NcSrcMigration {
-
- public function __construct(array $arguments) {
- parent::__construct($arguments);
-
- // Event Date field.
- $this->addFieldMapping('field_event_date', 'field_event_date');
- $this->addFieldMapping('field_event_date:to', 'field_event_date:value2');
- $this->addFieldMapping('field_event_date:timezone')
- ->defaultValue($this->ncsrc_timezone);
- $this->addUnmigratedDestinations(array(
- 'field_event_date:rrule',
- ));
-
- // Thumbnail image field.
- $this->addFieldMapping('field_global_image', 'filepath');
- $this->addFieldMapping('field_global_image:file_class')
- ->defaultValue('NcSrcMigrateFileUri');
- $this->addFieldMapping('field_global_image:source_dir')
- ->defaultValue($this->ncsrc_source_site);
- $this->addUnmigratedDestinations(array(
- 'field_global_image:language',
- // ...
- 'field_global_image:title',
- ));
-
- // Contact URL field.
- // Пример миграции поля типа Link.
- $this->addFieldMapping('field_event_contact_url', 'field_event_url');
- $this->addFieldMapping('field_event_contact_url:title', 'field_event_url:title');
- $this->addFieldMapping('field_event_contact_url:attributes', 'field_event_url:attributes');
- $this->addUnmigratedDestinations(array(
- 'field_event_contact_url:language'
- ));
-
- // Session field (reference to Session).
- // Ссылка на ноду с "Выступлением".
- $this->addFieldMapping('field_event_session', 'field_agenda_reference')
- ->sourceMigration('SessionNode');
-
- // Focus Area field.
- // Маппинг словарей таксономии. В D6 таксономия устроена немного по-другом, поэтому
- // с Source прилетает ID словаря, а не филд. ID словаря был прописан в моей базовом классе.
- $this->addFieldMapping('field_global_focus_area',
- self::NCSRC_MIGRATION_SOURCE_FOCUS_AREAS_VID);
-
- // State field.
- $this->addFieldMapping('field_global_state',
- self::NCSRC_MIGRATION_SOURCE_STATES_VID);
-
- // Non-migrated Destinations.
- $this->addUnmigratedDestinations(array(
- 'field_global_audience',
- // ...
- 'field_event_location_short:language',
- ));
-
- // Non-migrated Sources.
- $this->addUnmigratedSources(array(
- 'uid',
- // ...
- 'totalcount',
- ));
-
- // Removes mappings to prevent warning messages.
- $this->removeFieldMapping('pathauto');
- }
-
- // Тут был query(), ничего интересного.
-
- public function prepare(&$row) {
- // ...
-
- // Set Audience terms if they exist after re-tagging process.
- // Какая-то очередная магия с терминами таксономии.
- if (isset($this->sourceValues->{self::NCSRC_MIGRATION_SOURCE_AUDIENCE_VID})) {
- if (!isset($row->field_global_audience[LANGUAGE_NONE])) {
- $row->field_global_audience[LANGUAGE_NONE] = array();
- }
-
- foreach ($this->sourceValues->{self::NCSRC_MIGRATION_SOURCE_AUDIENCE_VID} as $tid) {
- $row->field_global_audience[LANGUAGE_NONE][] = array('target_id' => $tid);
- }
- }
-
- // Updates email field to prevent validation errors during migrate node #2288.
- if (!empty($row->field_event_email[LANGUAGE_NONE][0]['email'])) {
- $email =& $row->field_event_email[LANGUAGE_NONE][0]['email'];
- $email = preg_replace('/([.\s]+)$/', '', $email);
- }
- }
-
- public function prepareRow($current_row) {
- // Always start your prepareRow implementation with this clause. You need to
- // be sure your parent classes have their chance at the row, and that if
- // they return FALSE (indicating the row should be skipped) you pass that
- // on.
- if (parent::prepareRow($current_row) === FALSE) {
- return FALSE;
- }
-
- // This field is changed from Taxonomy to Boolean at Destination.
- $current_row->field_by_ncsrc = !empty($current_row->field_by_ncsrc) ? 1 : 0;
-
- // Taxonomy re-tagging.
- $vids = array(
- self::NCSRC_MIGRATION_SOURCE_FOCUS_AREAS_VID,
- self::NCSRC_MIGRATION_SOURCE_STATES_VID,
- );
- // Вызываем метод из моего базового класса, чтобы обработать термины.
- $this->taxonomyReTagging($current_row, $vids);
-
- // Private field migration.
- $current_row->field_private_event = !empty($current_row->field_private_event) ? 1 : 0;
-
- return TRUE;
- }
- }
Еще один интересный момент из миграции типа контента Webinar:
- class NcSrcWebinarNodeMigration extends NcSrcMigration {
-
- // Остальные методы класса удалены, так как там ничего нового и интересного.
-
- // Итак, нода уже сохранена, но необходимо внести кое-какие изменения
- // в нее саму или же в связанные сущности.
- public function complete($node, stdClass $source_row) {
- parent::complete($node, $source_row);
-
- // Updates File fields after node saving.
- // Problem is that we use File entity with fields.
- // Fields of file can't be updated during node saving.
- // So we may update file entities after node saving.
- if (!empty($node->field_webinar_attachments[LANGUAGE_NONE])) {
- foreach ($node->field_webinar_attachments[LANGUAGE_NONE] as $file_data) {
- if ($file = file_load($file_data['fid'])) {
- // Compare filepath at Destination and Source to ensure that files are identical.
- foreach ($source_row->field_files_optional as $key => $old_filepath) {
- preg_match('/^.*\/([^\/]+)\.[a-zA-Z]+$/is', $old_filepath, $matches);
- $old_filename = isset($matches[1]) ? $matches[1] : '';
-
- if (!empty($old_filename) && strpos($file->uri, $old_filename)) {
- // Set description and save file.
- $file->field_file_description[LANGUAGE_NONE][0]['value'] =
- $source_row->{'field_files_optional:description'}[$key];
- file_save($file);
- break;
- }
- }
- }
- }
- }
- }
- }
Если я не путаю, то вышеприведенный код решает следующую проблему. На Destination сайте стоит File Entity модуль. У сущности File есть поле с описанием, однако мы не мигрируем все файлы, а только те, которые связаны с нодой Webinar. При создании ноды Webinar с файлом, создается также сущность File, которую, собствено, мы и обновляем в методе
.complete()
Фух.. Вот, наверное, и все нестандартные приемы миграции, с которыми я столкнулся при переносе контента с Drupal 6 на Drupal 7.
Используйте Drush для Migrate
Да, ребятки, запускайте ваши миграции через консоль. Во-первых, Drush предоставляет больше опций для запуска процесса миграции (например, можно мигрировать объекты с указанным ID). Во-вторых, при миграции большого числа данных процесс займет меньше времени и не завалится, как это бывает с запуском в браузере. Короче вот вам ниже парочка ссылок — не хочу все это переводить и переписывать. Тем более там и так все складно и понятно:
Все, моя совесть наконец-то чиста: я поделился всем, что сам узнал о Migrate. Не знаю, насколько еще актуален этот пост — большую часть поста я написал еще полгода назад, но никак не мог его закончить. Надеюсь, говнокода никто не узрел в моих листингах — я старался как мог ;)
Всем удачи и не затягивайте с изучением Drupal 8.. как это делаю я!
Комментарии
Перенос прошел без проблем))
Добавить комментарий