Миграция контента из Drupal 6 в 7 при помощи Migrate API

Миграция контента в Drupal

Привет, друпалеры! С трудом нашел несколько часов, чтобы дописать этот пост: прям разрываюсь между 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?

Перенос сайта с Drupal 6 на 7

Фреймворк Migrate, в отличие от нынешнего Drupal 7, написан с использованием ООП, а это значит, что вам необходимо понимать несколько вещей: что такое "класс", "метод" и "наследование". Таким образом, модуль предоставляют набор готовых классов, призванных упростить миграцию данных. Sub-модули предоставляют дополнительные классы, которые унаследованы от базовых модуля Migrate и имеют более функциональные методы под конкретную задачу.

Что такое Source & Destination?

Чисто интуитивно уже можно догадаться: Source — это источник с данными, Destination — это ваша база данных, куда необходимо перетянуть данные. Эти понятия — можно сказать, основа идеологии Migrate. Ваша задача, как раз и состоит, в том, чтобы настроить правила миграции ("маппинг" ин инглиш) из Source в Destination. Migrate позаботился даже о программистах-кликерах: базовый маппинг вы можете настроить через админку. Однако админка — это лишь вершина айсберга по сравнению с тем, что можно вытворять в собственных классах.

Маппинг полей через интерфейс

Раз я рассказываю про миграцию Drupal-to-Drupal, то пора бы уже пролить свет и на то, как же подключиться в базе данных D6. У вас есть два варианта: вы можете поднять локально дамп БД или же подключаться прямо к продакшену. Настройки подключения к базе данных D6 рекомендуется внести прямо в settings.php — просто добавьте в массив $database еще один массив, например, с ключом 'legacy'. Далее вам придется указывать этот самый ключ 'legacy' в качестве значения для 'source_connection' (это приблуда от модуля migrate_d2d).

Вот. Теперь, будем, считать ваш Drupal знает и про Source, и про Destination базы данных. Самое время создать модуль и настроить маппинг.

Создание модуля миграции на базе Migrate API

Итак, оставим килознаки теории и перейдем к практике. Модуль, с которого будут приведены примеры, у меня, если что, называется 'ncsrc_migration'. Info-файл модуля ничего необычного не содержит, кроме того, что необходимо подключать файлы с классами миграции:

  1. name = NCSRC Migration
  2. description = Migration of content from old site.
  3. package = NCSRC
  4. core = 7.x
  5.  
  6. dependencies[] = migrate (>=7.x-2.7)
  7. dependencies[] = migrate_d2d (>=7.x-2.1)
  8.  
  9. # Это файл с общими и абстрактными классами.
  10. files[] = includes/ncsrc_migration.general.inc
  11. # Класс для миграции материалов типа "Докладчик".
  12. files[] = includes/ncsrc_migration.presenter_ct.inc
  13. # Класс для миграции материалов типа "Выступление".
  14. files[] = includes/ncsrc_migration.session_ct.inc
  15. # Класс для миграции материалов типа "Конференция".
  16. files[] = includes/ncsrc_migration.event_ct.inc
  17. # Класс для миграции материалов типа "Публикация".
  18. files[] = includes/ncsrc_migration.publication_ct.inc
  19. # Класс для миграции материалов типа "Рассылка",
  20. # которые на новый сайт будут перенесены как "Публикация".
  21. files[] = includes/ncsrc_migration.newsletter_ct.inc

Основной файл модуля 'ncsrc_migration.module' у меня так и остался пустым. Имплементацию хука hook_migrate_api, согласно канонам, лучше закинуть в файл 'ncsrc_migration.migrate.inc':

  1. /**
  2.  * Implements hook_migrate_api().
  3.  */
  4. function ncsrc_migration_migrate_api() {
  5. /**
  6.   * Declare the api version and migration group.
  7.   */
  8. $api = array(
  9. 'api' => 2,
  10. // Тут описываем группы миграции. Например, можно объединить в одну
  11. // группу миграции, связанные с конференциями, выступлениями и докладчиками.
  12. 'groups' => array(
  13. 'event_ct' => array(
  14. 'title' => t('Content type: Event, Session'),
  15. ),
  16. 'publication_ct' => array(
  17. 'title' => t('Content type: Publication'),
  18. ),
  19. ),
  20. // Описание классов миграции. Напомнимаю, что все классы будут унаследованы
  21. // от расширенных классов, описанных в 'migrate_d2d' модуле. Поэтому достпуны
  22. // некоторые доп. опции такие, как 'source_type' и 'destination_type'.
  23. 'migrations' => array(
  24. 'PresenterNode' => array(
  25. 'class_name' => 'NcSrcPresenterNodeMigration',
  26. 'group_name' => 'event_ct',
  27. 'description' => t('Migration of nodes of Presenter content type.'),
  28. // Машинное имя Content type на Drupal 6 сайте.
  29. 'source_type' => 'presenter',
  30. // Машинное имя Content type на Drupal 7 сайте.
  31. 'destination_type' => 'presenter',
  32. ),
  33. 'SessionNode' => array(
  34. 'class_name' => 'NcSrcSessionNodeMigration',
  35. 'group_name' => 'event_ct',
  36. 'description' => t('Migration of nodes of Session content type.'),
  37. 'source_type' => 'agenda',
  38. 'destination_type' => 'session',
  39. // Вот тут указываем, что миграция нод типа "Выступление"
  40. // зависит от миграции нод типа "Докладчик". Другими словами,
  41. // пока не перетянете всех докладчиков Migrate не даст тянуть выступления.
  42. 'dependencies' => array(
  43. 'PresenterNode',
  44. ),
  45. ),
  46. 'EventNode' => array(
  47. 'class_name' => 'NcSrcEventNodeMigration',
  48. 'group_name' => 'event_ct',
  49. 'description' => t('Migration of nodes of Event content type.'),
  50. 'source_type' => 'event',
  51. 'destination_type' => 'event',
  52. 'dependencies' => array(
  53. 'SessionNode',
  54. ),
  55. ),
  56. 'PublicationNode' => array(
  57. 'class_name' => 'NcSrcPublicationNodeMigration',
  58. 'group_name' => 'publication_ct',
  59. 'description' => t('Migration of nodes of Publication content type.'),
  60. 'source_type' => 'resource',
  61. 'destination_type' => 'publication',
  62. ),
  63. 'NewsletterNode' => array(
  64. 'class_name' => 'NcSrcNewsletterNodeMigration',
  65. 'group_name' => 'publication_ct',
  66. 'description' => t('Migration of nodes of Newsletter content type.'),
  67. 'source_type' => 'newsletter',
  68. // Вот тут можете обратить внимание, что миграция,
  69. // как и в предыдущем случае будет осуществляеться в один и тот же контент тип.
  70. 'destination_type' => 'publication',
  71. ),
  72. ),
  73. );
  74.  
  75. return $api;
  76. }

На самом деле имплементация хука hook_migrate_api — это самое простое. Теперь необходимо описать все указанные классы, да при этом еще соблюдая все требования Migrate API. Начну я пожалуй со вспомогательного файла 'ncsrc_migration.general.inc':

  1. // Вот он базовый класс для всех остальных моих классов миграции.
  2. // Как видите, идет наследование от DrupalNode6Migration - именно этот класс и
  3. // предоставляет модуль 'migrate_d2d'.
  4. abstract class NcSrcMigration extends DrupalNode6Migration {
  5. // Всякие необходимые константы. Например, мне нужны были ID словарей таксономии.
  6. const NCSRC_MIGRATION_SOURCE_FOCUS_AREAS_VID = '999';
  7.  
  8. // Кастомные свойства класса.
  9. protected $ncsrc_source_site;
  10. protected $ncsrc_timezone;
  11. protected $ncsrc_text_format = 'filtered_html';
  12.  
  13. // Конструктор для определение свойств класса.
  14. public function __construct(array $arguments) {
  15. // Site's default timezone.
  16. $this->ncsrc_timezone = variable_get('date_default_timezone', '');
  17.  
  18. // Прочий код..
  19. // Кстати, аргументы 'source_version' и 'source_connection' обязательны.
  20. // Без них класс DrupalNode6Migration будет жутко ругаться.
  21. // Однако, из можно указать и в hook_migrate_api, если не ошибаюсь.
  22.  
  23. // Version of old site's Drupal.
  24. $arguments['source_version'] = '6';
  25.  
  26. // Database connection. See settings.php
  27. $arguments['source_connection'] = $this->ncsrc_source_db;
  28.  
  29. parent::__construct($arguments);
  30. }
  31.  
  32. // Далее идет серия методов для обработки терминов таксономии.
  33. // Например, на старом сайте было 3 термина А, Б, В.
  34. // На новом сайте должен остаться только термин Б. Причем все ноды,
  35. // имеющие связи с А и В, должны переключиться на Б.
  36. // Это как пример того, что позволяет делать Migrate.
  37. protected function taxonomyReTagging(&$row, $vids) { ... }
  38. }
  39.  
  40. // В рамках проекта пришлось переопределять класс MigrateFileUri,
  41. // т.к. он не позволял забирать файлы с сайта, который находился
  42. // на серваке с HTTP авторизацией. Т.е. URL'ы вида
  43. // http://user:pass@domain.com/path_to_file обрабатывались некорректно.
  44. // Опять же приведено, как пример гибкости Migrate.
  45. class NcSrcMigrateFileUri extends MigrateFileUri {
  46. static public function urlencode($filename) { ... }
  47. protected function copyFile($destination) { ... }
  48. }

Понимаю, что с первого взгляда нихрена не понятно, но я не буду останавливаться и продолжу валить листингами кода. Читайте комментарии к коду. Начнем с класса миграции типа контента Докладчик (он же Presenter):

  1. // Данный класс наследует вышеописанный базовый класс, как я и обещал.
  2. class NcSrcPresenterNodeMigration extends NcSrcMigration {
  3.  
  4. public function __construct(array $arguments) {
  5. parent::__construct($arguments);
  6.  
  7. // Image field.
  8. // @see beer.inc in migrate_example module.
  9. $this->addFieldMapping('field_presenter_image', 'filepath');
  10. $this->addFieldMapping('field_presenter_image:file_class')
  11. ->defaultValue('NcSrcMigrateFileUri');
  12. $this->addFieldMapping('field_presenter_image:source_dir')
  13. ->defaultValue($this->ncsrc_source_site);
  14.  
  15. $this->addUnmigratedDestinations(array(
  16. 'field_presenter_image:language',
  17. 'field_presenter_image:preserve_files',
  18. 'field_presenter_image:destination_dir',
  19. 'field_presenter_image:destination_file',
  20. 'field_presenter_image:file_replace',
  21. 'field_presenter_image:urlencode',
  22. 'field_presenter_image:alt',
  23. 'field_presenter_image:title'
  24. ));
  25.  
  26. // Job Title field.
  27. $this->addFieldMapping('field_presenter_job', 'field_job_title_presenter');
  28. $this->addUnmigratedDestinations(array('field_presenter_job:language'));
  29.  
  30. // Organization Link field.
  31. $this->addFieldMapping('field_presenter_org_link', 'field_organization_presenter');
  32. $this->addFieldMapping('field_presenter_org_link:title', 'field_organization_presenter:title');
  33. $this->addUnmigratedDestinations(
  34. array('field_presenter_org_link:attributes', 'field_presenter_org_link:language'));
  35.  
  36. // Non-migrated Sources.
  37. $this->addUnmigratedSources(array(
  38. 'uid',
  39. // ...
  40. 'totalcount',
  41. ));
  42.  
  43. // Removes mappings to prevent warning messages.
  44. $this->removeFieldMapping('body:language');
  45. $this->removeFieldMapping('pathauto');
  46. }
  47.  
  48. protected function query() {
  49. $query = parent::query();
  50.  
  51. // Updates query for image migration.
  52. // @todo: will be broken with multiple values.
  53. // Короче так делать плохо, ибо если у поля несколько значений, то будут дубли
  54. // строк в результатх запроса и как следствие в Destination базе окажется
  55. // лишь последнее значение вместо всех. Для single значений в принципе прокатит.
  56. $query->leftJoin('files', 'files', 'f.field_presenter_img_fid = files.fid');
  57. $query->fields('files', array('filepath'));
  58.  
  59. return $query;
  60. }
  61.  
  62. function prepare(&$row) {
  63. // Required for disabling alias generating by Pathauto module.
  64. $row->path['pathauto'] = 0;
  65. }
  66. }

Видимо, все же придется остановиться и прояснить некоторый моменты. По сути ваш класс миграции — это не только маппинг полей, но и возможность адаптировать миграцию под вашу конкретную задачу. Каждый метод в этом примере расширяет функционал метода из родительского класса, который предоставляет Migrate. Если говорить языком Drupal'a, то представьте, что эти методы — это хуки, которые мы привыкли имплементировать. Попробую немного объяснить какой метод за что отвечает и что в него надо писать.

Основные методы классов Migrate

__construct() — конструктор класса, стреляет сразу же, когда дело доходит до инициализации вашего класса. Не забывайте включать parent::__construct($arguments); в начало вашего кода, иначе рискуете потерять важные данные из родительских классов. В этом методе настраивается, как правило, маппинг полей. Откуда я взял все эти ключи полей? Принцип довольно прост: открываете в админке Task (термин взял как раз из интерфейса) с этой миграцией и глядите на что ругается вам система: какие Sorce поля не описаны, какие Destination поля остались неиспользуемыми. Собственно ваша задача настроить маппинг так, чтобы в интерфейсе не было никаких красных шрифтов с ошибками и предупреждениями. Для этого у вас есть набор методов, таких как addFieldMapping(), addUnmigratedDestinations(), removeFieldMapping().

query() — это ваша возможность "альтернуть" запрос, которым будут выгребаться данные из Source таблицы. Результат конечного запроса вы кстати опять же можете глянуть в интерфейсе на вкладке Source. В общем, этот метод нужен для того, чтобы вытягивать из Source базы больше данных, чем это делают родительские классы. На самом деле, если не включать $query = parent::query();, то вы можете написать свой query с нуля сами.

prepareRow() — отрабатывает после query(), позволяет изменить Source данные, которые придут на маппинг. Если у вас не получается одним запросом выгрести все данные из Source базы, то в этом методе можете инициировать еще несколько дополнительных запросов. Конечно лучше поработать над основным запросом в query(), но не всегда это удается. Чуть ниже вы увидите пример с этим методом.

prepare() — стреляет перед сохранением объекта в Destination базу. Это один из последних этапов миграции: данные из Source вытянуты, преобразованы согласно маппингу и готовы к сохранению. В вышеприведенном коде мне надо было отключить генерацию алиаса, чтобы использовался алиас с Sorce сайта.

complete() — стреляет после того, как объект сущности уже сохранен в Destination. Я был очень удивлен, когда уперся в его необходимость и нашел его в родительских классах. Все же приятно, когда ты только захотел, а оно уже и есть. Пример использования увидите ниже в миграции типа Webinar.

Методы разместил в порядке их выполнения. Теперь опять перейдем к наглядным примерам. Класс для миграции ноды типа "Доклад" (Session в оригинале):

  1. class NcSrcSessionNodeMigration extends NcSrcMigration {
  2.  
  3. public function __construct(array $arguments) {
  4. parent::__construct($arguments);
  5.  
  6. // Attachment field.
  7. $this->addFieldMapping('field_session_attachment', 'field_downloads');
  8. $this->addFieldMapping('field_session_attachment:file_class')
  9. ->defaultValue('NcSrcMigrateFileUri');
  10. $this->addFieldMapping('field_session_attachment:source_dir')
  11. ->defaultValue($this->ncsrc_source_site);
  12.  
  13. $this->addUnmigratedDestinations(array(
  14. 'field_session_attachment:language',
  15. // ...
  16. 'field_session_attachment:display',
  17. ));
  18.  
  19. // Date Published field.
  20. $this->addFieldMapping('field_session_date', 'field_agenda_date');
  21. $this->addFieldMapping('field_session_date:to', 'field_agenda_date:value2');
  22. $this->addFieldMapping('field_session_date:timezone')
  23. ->defaultValue($this->ncsrc_timezone);
  24. $this->addUnmigratedDestinations(array(
  25. 'field_session_date:rrule',
  26. ));
  27.  
  28. // Session Location field.
  29. $this->addFieldMapping('field_session_location', 'field_location');
  30. $this->addFieldMapping('field_session_location:format')
  31. ->defaultValue($this->ncsrc_text_format);;
  32. $this->addUnmigratedDestinations(array(
  33. 'field_session_location:language',
  34. ));
  35.  
  36. // Speaker field (reference to Presenter).
  37. // А вот так легко можно свзять одну ноду с другой через Entity Reference
  38. // поле, например. Напоминаю, что Presenter мигрируется до этого типа
  39. // контента.
  40. $this->addFieldMapping('field_session_speaker', 'field_presenter_references')
  41. ->sourceMigration('PresenterNode');
  42.  
  43. // Non-migrated Destinations.
  44. $this->addUnmigratedDestinations(array(
  45. 'field_session_day',
  46. // ...
  47. 'field_session_video:display',
  48. ));
  49.  
  50. // Non-migrated Sources.
  51. $this->addUnmigratedSources(array(
  52. 'uid',
  53. // ...
  54. 'totalcount',
  55. ));
  56.  
  57. // Removes mappings to prevent warning messages.
  58. $this->removeFieldMapping('pathauto');
  59. }
  60.  
  61. public function prepare(&$row) {
  62. // Required for disabling alias generating by Pathauto module.
  63. $row->path['pathauto'] = 0;
  64.  
  65. // Изменяем текстовый формат - хороший пример того, чем еще может быть
  66. // полезен данный метод в процессе миграции.
  67. $row->body[LANGUAGE_NONE][0]['value_format'] = $this->ncsrc_text_format;
  68. $row->body[LANGUAGE_NONE][0]['format'] = $this->ncsrc_text_format;
  69.  
  70. // Тут был какой-то головняк с миграцией дат, пришлось изобретать.
  71. if (isset($row->field_session_date[LANGUAGE_NONE][0])) {
  72. $value = $row->field_session_date[LANGUAGE_NONE][0]['value'];
  73.  
  74. $timezone = new DateTimeZone($this->ncsrc_timezone);
  75. $date = new DateObject($value, $timezone);
  76. $offset = $timezone->getOffset($date);
  77.  
  78. $row->field_session_date[LANGUAGE_NONE][0]['value'] += $offset;
  79. $row->field_session_date[LANGUAGE_NONE][0]['value2'] += $offset;
  80. }
  81. }
  82.  
  83. public function prepareRow($current_row) {
  84. // Always start your prepareRow implementation with this clause. You need to
  85. // be sure your parent classes have their chance at the row, and that if
  86. // they return FALSE (indicating the row should be skipped) you pass that
  87. // on.
  88. // Короче это важная штука, ее надо ОБЯЗАТЕЛЬНО вставлять,
  89. // если определяете данный метод в своем классе.
  90. if (parent::prepareRow($current_row) === FALSE) {
  91. return FALSE;
  92. }
  93.  
  94. // Переключаемся на Source БД.
  95. db_set_active($this->ncsrc_source_db);
  96.  
  97. // Собственно, вот он рабочий вариант для миграции файловых
  98. // полей с несколькими значенияи (multi value).
  99. $query = db_select('content_field_downloads', 'fd');
  100. $query->leftJoin('files', 'f', 'fd.field_downloads_fid = f.fid');
  101. $query->fields('f', array('filepath'));
  102. $query->condition('fd.vid', $current_row->vid);
  103. $files_result = $query->execute();
  104.  
  105. // Опять тут с датами какая-то алхимия.
  106. $query = db_select('content_field_agenda_reference', 'far');
  107. $query->leftJoin('content_field_event_date', 'fed', 'fed.vid = far.vid');
  108. $query->fields('fed', array('field_event_date_value'));
  109. $query->condition('far.field_agenda_reference_nid', $current_row->nid);
  110. $query->orderBy('far.nid', 'ASC');
  111. $query->range(0, 1);
  112. $date_result = $query->execute()->fetchField();
  113.  
  114. db_set_active();
  115.  
  116. $current_row->field_downloads = array();
  117. foreach ($files_result as $row) {
  118. $current_row->field_downloads[] = $row->filepath;
  119. }
  120.  
  121. $current_row->field_event_date_value = $date_result;
  122.  
  123. return TRUE;
  124. }
  125. }

Заключительный листинг миграции контент типа Event для связки "Конференция" - "Выступление" - "Докладчик":

  1. class NcSrcEventNodeMigration extends NcSrcMigration {
  2.  
  3. public function __construct(array $arguments) {
  4. parent::__construct($arguments);
  5.  
  6. // Event Date field.
  7. $this->addFieldMapping('field_event_date', 'field_event_date');
  8. $this->addFieldMapping('field_event_date:to', 'field_event_date:value2');
  9. $this->addFieldMapping('field_event_date:timezone')
  10. ->defaultValue($this->ncsrc_timezone);
  11. $this->addUnmigratedDestinations(array(
  12. 'field_event_date:rrule',
  13. ));
  14.  
  15. // Thumbnail image field.
  16. $this->addFieldMapping('field_global_image', 'filepath');
  17. $this->addFieldMapping('field_global_image:file_class')
  18. ->defaultValue('NcSrcMigrateFileUri');
  19. $this->addFieldMapping('field_global_image:source_dir')
  20. ->defaultValue($this->ncsrc_source_site);
  21. $this->addUnmigratedDestinations(array(
  22. 'field_global_image:language',
  23. // ...
  24. 'field_global_image:title',
  25. ));
  26.  
  27. // Contact URL field.
  28. // Пример миграции поля типа Link.
  29. $this->addFieldMapping('field_event_contact_url', 'field_event_url');
  30. $this->addFieldMapping('field_event_contact_url:title', 'field_event_url:title');
  31. $this->addFieldMapping('field_event_contact_url:attributes', 'field_event_url:attributes');
  32. $this->addUnmigratedDestinations(array(
  33. 'field_event_contact_url:language'
  34. ));
  35.  
  36. // Session field (reference to Session).
  37. // Ссылка на ноду с "Выступлением".
  38. $this->addFieldMapping('field_event_session', 'field_agenda_reference')
  39. ->sourceMigration('SessionNode');
  40.  
  41. // Focus Area field.
  42. // Маппинг словарей таксономии. В D6 таксономия устроена немного по-другом, поэтому
  43. // с Source прилетает ID словаря, а не филд. ID словаря был прописан в моей базовом классе.
  44. $this->addFieldMapping('field_global_focus_area',
  45. self::NCSRC_MIGRATION_SOURCE_FOCUS_AREAS_VID);
  46.  
  47. // State field.
  48. $this->addFieldMapping('field_global_state',
  49. self::NCSRC_MIGRATION_SOURCE_STATES_VID);
  50.  
  51. // Non-migrated Destinations.
  52. $this->addUnmigratedDestinations(array(
  53. 'field_global_audience',
  54. // ...
  55. 'field_event_location_short:language',
  56. ));
  57.  
  58. // Non-migrated Sources.
  59. $this->addUnmigratedSources(array(
  60. 'uid',
  61. // ...
  62. 'totalcount',
  63. ));
  64.  
  65. // Removes mappings to prevent warning messages.
  66. $this->removeFieldMapping('pathauto');
  67. }
  68.  
  69. // Тут был query(), ничего интересного.
  70.  
  71. public function prepare(&$row) {
  72. // ...
  73.  
  74. // Set Audience terms if they exist after re-tagging process.
  75. // Какая-то очередная магия с терминами таксономии.
  76. if (isset($this->sourceValues->{self::NCSRC_MIGRATION_SOURCE_AUDIENCE_VID})) {
  77. if (!isset($row->field_global_audience[LANGUAGE_NONE])) {
  78. $row->field_global_audience[LANGUAGE_NONE] = array();
  79. }
  80.  
  81. foreach ($this->sourceValues->{self::NCSRC_MIGRATION_SOURCE_AUDIENCE_VID} as $tid) {
  82. $row->field_global_audience[LANGUAGE_NONE][] = array('target_id' => $tid);
  83. }
  84. }
  85.  
  86. // Updates email field to prevent validation errors during migrate node #2288.
  87. if (!empty($row->field_event_email[LANGUAGE_NONE][0]['email'])) {
  88. $email =& $row->field_event_email[LANGUAGE_NONE][0]['email'];
  89. $email = preg_replace('/([.\s]+)$/', '', $email);
  90. }
  91. }
  92.  
  93. public function prepareRow($current_row) {
  94. // Always start your prepareRow implementation with this clause. You need to
  95. // be sure your parent classes have their chance at the row, and that if
  96. // they return FALSE (indicating the row should be skipped) you pass that
  97. // on.
  98. if (parent::prepareRow($current_row) === FALSE) {
  99. return FALSE;
  100. }
  101.  
  102. // This field is changed from Taxonomy to Boolean at Destination.
  103. $current_row->field_by_ncsrc = !empty($current_row->field_by_ncsrc) ? 1 : 0;
  104.  
  105. // Taxonomy re-tagging.
  106. $vids = array(
  107. self::NCSRC_MIGRATION_SOURCE_FOCUS_AREAS_VID,
  108. self::NCSRC_MIGRATION_SOURCE_STATES_VID,
  109. );
  110. // Вызываем метод из моего базового класса, чтобы обработать термины.
  111. $this->taxonomyReTagging($current_row, $vids);
  112.  
  113. // Private field migration.
  114. $current_row->field_private_event = !empty($current_row->field_private_event) ? 1 : 0;
  115.  
  116. return TRUE;
  117. }
  118. }

Еще один интересный момент из миграции типа контента Webinar:

  1. class NcSrcWebinarNodeMigration extends NcSrcMigration {
  2.  
  3. // Остальные методы класса удалены, так как там ничего нового и интересного.
  4.  
  5. // Итак, нода уже сохранена, но необходимо внести кое-какие изменения
  6. // в нее саму или же в связанные сущности.
  7. public function complete($node, stdClass $source_row) {
  8. parent::complete($node, $source_row);
  9.  
  10. // Updates File fields after node saving.
  11. // Problem is that we use File entity with fields.
  12. // Fields of file can't be updated during node saving.
  13. // So we may update file entities after node saving.
  14. if (!empty($node->field_webinar_attachments[LANGUAGE_NONE])) {
  15. foreach ($node->field_webinar_attachments[LANGUAGE_NONE] as $file_data) {
  16. if ($file = file_load($file_data['fid'])) {
  17. // Compare filepath at Destination and Source to ensure that files are identical.
  18. foreach ($source_row->field_files_optional as $key => $old_filepath) {
  19. preg_match('/^.*\/([^\/]+)\.[a-zA-Z]+$/is', $old_filepath, $matches);
  20. $old_filename = isset($matches[1]) ? $matches[1] : '';
  21.  
  22. if (!empty($old_filename) && strpos($file->uri, $old_filename)) {
  23. // Set description and save file.
  24. $file->field_file_description[LANGUAGE_NONE][0]['value'] =
  25. $source_row->{'field_files_optional:description'}[$key];
  26. file_save($file);
  27. break;
  28. }
  29. }
  30. }
  31. }
  32. }
  33. }
  34. }

Если я не путаю, то вышеприведенный код решает следующую проблему. На Destination сайте стоит File Entity модуль. У сущности File есть поле с описанием, однако мы не мигрируем все файлы, а только те, которые связаны с нодой Webinar. При создании ноды Webinar с файлом, создается также сущность File, которую, собствено, мы и обновляем в методе complete().

Фух.. Вот, наверное, и все нестандартные приемы миграции, с которыми я столкнулся при переносе контента с Drupal 6 на Drupal 7.

Используйте Drush для Migrate

Да, ребятки, запускайте ваши миграции через консоль. Во-первых, Drush предоставляет больше опций для запуска процесса миграции (например, можно мигрировать объекты с указанным ID). Во-вторых, при миграции большого числа данных процесс займет меньше времени и не завалится, как это бывает с запуском в браузере. Короче вот вам ниже парочка ссылок — не хочу все это переводить и переписывать. Тем более там и так все складно и понятно:

Все, моя совесть наконец-то чиста: я поделился всем, что сам узнал о Migrate. Не знаю, насколько еще актуален этот пост — большую часть поста я написал еще полгода назад, но никак не мог его закончить. Надеюсь, говнокода никто не узрел в моих листингах — я старался как мог ;)

Всем удачи и не затягивайте с изучением Drupal 8.. как это делаю я!

Комментарии

Аватар пользователя seoonly
seoonly

Перенос прошел без проблем))

Добавить комментарий

 8888888888P            888        .d88888b.  
d88P 888 d88P" "Y88b
d88P 888 888 888
d88P 888 888 88888b. 888 888
d88P 888 888 888 "88b 888 888
d88P 888 888 888 888 888 Y8b 888
d88P Y88b 888 888 888 Y88b.Y8b88P
d8888888888 "Y88888 888 888 "Y888888"
888 Y8b
Y8b d88P
"Y88P"
Зарегистрируйтесь для добавления материалов без проверки.