Миграция контента из 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

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

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

                               888 
888
888
88888b. 888 888 88888b. 888
888 "88b `Y8bd8P' 888 "88b 888
888 888 X88K 888 888 888
888 888 .d8""8b. 888 888 888
888 888 888 888 888 888 888


Зарегистрируйтесь для добавления материалов без проверки.