$npx -y skills add edutrul/drupal-ai --skill drupal-migrationsDrupal migration API — D7-to-D11 upgrades, CSV imports, JSON API imports, and custom source/process/destination plugins.
| 1 | # Drupal Migrations |
| 2 | |
| 3 | ## Migration YAML Structure |
| 4 | |
| 5 | ```yaml |
| 6 | id: my_migration |
| 7 | label: 'My Migration' |
| 8 | migration_group: my_group # group related migrations together |
| 9 | |
| 10 | source: |
| 11 | plugin: source_plugin_name |
| 12 | |
| 13 | process: |
| 14 | destination_field: source_field |
| 15 | |
| 16 | destination: |
| 17 | plugin: 'entity:node' |
| 18 | default_bundle: article |
| 19 | |
| 20 | migration_dependencies: |
| 21 | required: |
| 22 | - other_migration # must match actual migration IDs |
| 23 | ``` |
| 24 | |
| 25 | ## Process Plugins Quick Reference |
| 26 | |
| 27 | | Plugin | Use for | |
| 28 | |---|---| |
| 29 | | `default_value` | Hardcode a value | |
| 30 | | `static_map` | Map old values → new values | |
| 31 | | `concat` | Join fields with a delimiter | |
| 32 | | `migration_lookup` | Reference a previously migrated entity (by old ID) | |
| 33 | | `entity_lookup` | Reference an existing entity by field value | |
| 34 | | `entity_generate` | Create entity if it doesn't exist | |
| 35 | | `skip_on_empty` | Skip field/row if value is empty | |
| 36 | | `skip_on_value` | Skip row if field equals a value | |
| 37 | | `sub_process` | Iterate a multi-value source array | |
| 38 | |
| 39 | ## Entity References |
| 40 | |
| 41 | ```yaml |
| 42 | process: |
| 43 | # Reference a previously migrated entity |
| 44 | uid: |
| 45 | plugin: migration_lookup |
| 46 | migration: users |
| 47 | source: author_id |
| 48 | |
| 49 | # Reference an existing entity by field value |
| 50 | field_category: |
| 51 | plugin: entity_lookup |
| 52 | source: category_name |
| 53 | entity_type: taxonomy_term |
| 54 | bundle: categories |
| 55 | bundle_key: vid |
| 56 | value_key: name |
| 57 | |
| 58 | # Create term if it doesn't exist |
| 59 | field_tags: |
| 60 | plugin: entity_generate |
| 61 | source: tag_name |
| 62 | entity_type: taxonomy_term |
| 63 | bundle: tags |
| 64 | value_key: name |
| 65 | ``` |
| 66 | |
| 67 | ## Multiple Values & Conditional |
| 68 | |
| 69 | ```yaml |
| 70 | process: |
| 71 | # Iterate a multi-value source array |
| 72 | field_tags: |
| 73 | plugin: sub_process |
| 74 | source: tags |
| 75 | process: |
| 76 | target_id: |
| 77 | plugin: entity_generate |
| 78 | source: name |
| 79 | entity_type: taxonomy_term |
| 80 | bundle: tags |
| 81 | value_key: name |
| 82 | |
| 83 | # Skip field processing if source is empty |
| 84 | field_image: |
| 85 | plugin: skip_on_empty |
| 86 | method: process |
| 87 | source: image_url |
| 88 | |
| 89 | # Skip entire row based on a value |
| 90 | pseudo_skip: |
| 91 | plugin: skip_on_value |
| 92 | source: status |
| 93 | method: row |
| 94 | value: 'draft' |
| 95 | ``` |
| 96 | |
| 97 | ## Custom Source Plugin |
| 98 | |
| 99 | ```php |
| 100 | namespace Drupal\my_module\Plugin\migrate\source; |
| 101 | |
| 102 | use Drupal\migrate\Attribute\MigrateSource; |
| 103 | use Drupal\migrate\Plugin\migrate\source\SqlBase; |
| 104 | use Drupal\migrate\Row; |
| 105 | |
| 106 | #[MigrateSource(id: 'legacy_products', source_module: 'my_module')] |
| 107 | final class LegacyProducts extends SqlBase { |
| 108 | |
| 109 | public function query() { |
| 110 | return $this->select('legacy_products', 'p') |
| 111 | ->fields('p', ['id', 'name', 'price']) |
| 112 | ->condition('p.status', 'active'); |
| 113 | } |
| 114 | |
| 115 | public function fields() { |
| 116 | return [ |
| 117 | 'id' => $this->t('Product ID'), |
| 118 | 'name' => $this->t('Name'), |
| 119 | 'price' => $this->t('Price'), |
| 120 | ]; |
| 121 | } |
| 122 | |
| 123 | public function getIds() { |
| 124 | return ['id' => ['type' => 'integer', 'alias' => 'p']]; |
| 125 | } |
| 126 | |
| 127 | public function prepareRow(Row $row) { |
| 128 | $row->setSourceProperty('price_with_tax', $row->getSourceProperty('price') * 1.21); |
| 129 | return parent::prepareRow($row); |
| 130 | } |
| 131 | |
| 132 | } |
| 133 | ``` |
| 134 | |
| 135 | ## Custom Process Plugin |
| 136 | |
| 137 | ```php |
| 138 | namespace Drupal\my_module\Plugin\migrate\process; |
| 139 | |
| 140 | use Drupal\migrate\Attribute\MigrateProcess; |
| 141 | use Drupal\migrate\MigrateExecutableInterface; |
| 142 | use Drupal\migrate\ProcessPluginBase; |
| 143 | use Drupal\migrate\Row; |
| 144 | |
| 145 | #[MigrateProcess(id: 'cents_to_decimal')] |
| 146 | final class CentsToDecimal extends ProcessPluginBase { |
| 147 | |
| 148 | public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { |
| 149 | if (empty($value) || !is_numeric($value)) { |
| 150 | return NULL; |
| 151 | } |
| 152 | return number_format((float) $value / 100, 2, '.', ''); |
| 153 | } |
| 154 | |
| 155 | } |
| 156 | ``` |
| 157 | |
| 158 | ## Key Drush Commands |
| 159 | |
| 160 | ```bash |
| 161 | drush migrate:status # list all migrations + status |
| 162 | drush migrate:import migration_id # run |
| 163 | drush migrate:import migration_id --update # re-run and update existing records |
| 164 | drush migrate:import migration_id --limit=50 # batch to avoid memory issues |
| 165 | drush migrate:rollback migration_id # undo |
| 166 | drush migrate:reset-status migration_id # fix "migration is busy" state |
| 167 | drush migrate:messages migration_id # show per-row errors |
| 168 | ``` |