1<?php 2 3namespace Drupal\migrate\Plugin; 4 5use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; 6use Drupal\Core\Plugin\ContainerFactoryPluginInterface; 7use Drupal\Core\Plugin\PluginBase; 8use Drupal\migrate\Exception\RequirementsException; 9use Drupal\migrate\MigrateException; 10use Drupal\migrate\MigrateSkipRowException; 11use Drupal\Component\Utility\NestedArray; 12use Symfony\Component\DependencyInjection\ContainerInterface; 13 14/** 15 * Defines the Migration plugin. 16 * 17 * The migration process plugin represents one single migration and acts like a 18 * container for the information about a single migration such as the source, 19 * process and destination plugins. 20 */ 21class Migration extends PluginBase implements MigrationInterface, RequirementsInterface, ContainerFactoryPluginInterface { 22 23 /** 24 * The migration ID (machine name). 25 * 26 * @var string 27 */ 28 protected $id; 29 30 /** 31 * The human-readable label for the migration. 32 * 33 * @var string 34 */ 35 protected $label; 36 37 /** 38 * The plugin ID for the row. 39 * 40 * @var string 41 */ 42 protected $row; 43 44 /** 45 * The source configuration, with at least a 'plugin' key. 46 * 47 * Used to initialize the $sourcePlugin. 48 * 49 * @var array 50 */ 51 protected $source; 52 53 /** 54 * The source plugin. 55 * 56 * @var \Drupal\migrate\Plugin\MigrateSourceInterface 57 */ 58 protected $sourcePlugin; 59 60 /** 61 * The configuration describing the process plugins. 62 * 63 * This is a strictly internal property and should not returned to calling 64 * code, use getProcess() instead. 65 * 66 * @var array 67 */ 68 protected $process = []; 69 70 /** 71 * The cached process plugins. 72 * 73 * @var array 74 */ 75 protected $processPlugins = []; 76 77 /** 78 * The destination configuration, with at least a 'plugin' key. 79 * 80 * Used to initialize $destinationPlugin. 81 * 82 * @var array 83 */ 84 protected $destination; 85 86 /** 87 * The destination plugin. 88 * 89 * @var \Drupal\migrate\Plugin\MigrateDestinationInterface 90 */ 91 protected $destinationPlugin; 92 93 /** 94 * The identifier map data. 95 * 96 * Used to initialize $idMapPlugin. 97 * 98 * @var string 99 */ 100 protected $idMap = []; 101 102 /** 103 * The identifier map. 104 * 105 * @var \Drupal\migrate\Plugin\MigrateIdMapInterface 106 */ 107 protected $idMapPlugin; 108 109 /** 110 * The source identifiers. 111 * 112 * An array of source identifiers: the keys are the name of the properties, 113 * the values are dependent on the ID map plugin. 114 * 115 * @var array 116 */ 117 protected $sourceIds = []; 118 119 /** 120 * The destination identifiers. 121 * 122 * An array of destination identifiers: the keys are the name of the 123 * properties, the values are dependent on the ID map plugin. 124 * 125 * @var array 126 */ 127 protected $destinationIds = []; 128 129 /** 130 * Specify value of source_row_status for current map row. Usually set by 131 * MigrateFieldHandler implementations. 132 * 133 * @var int 134 */ 135 protected $sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED; 136 137 /** 138 * Track time of last import if TRUE. 139 * 140 * @var bool 141 */ 142 protected $trackLastImported = FALSE; 143 144 /** 145 * These migrations must be already executed before this migration can run. 146 * 147 * @var array 148 */ 149 protected $requirements = []; 150 151 /** 152 * An optional list of tags, used by the plugin manager for filtering. 153 * 154 * @var array 155 */ 156 protected $migration_tags = []; 157 158 /** 159 * Whether the migration is auditable. 160 * 161 * If set to TRUE, the migration's IDs will be audited. This means that, if 162 * the highest destination ID is greater than the highest source ID, a warning 163 * will be displayed that entities might be overwritten. 164 * 165 * @var bool 166 */ 167 protected $audit = FALSE; 168 169 /** 170 * These migrations, if run, must be executed before this migration. 171 * 172 * These are different from the configuration dependencies. Migration 173 * dependencies are only used to store relationships between migrations. 174 * 175 * The migration_dependencies value is structured like this: 176 * @code 177 * array( 178 * 'required' => array( 179 * // An array of migration IDs that must be run before this migration. 180 * ), 181 * 'optional' => array( 182 * // An array of migration IDs that, if they exist, must be run before 183 * // this migration. 184 * ), 185 * ); 186 * @endcode 187 * 188 * @var array 189 */ 190 protected $migration_dependencies = []; 191 192 /** 193 * The migration's configuration dependencies. 194 * 195 * These store any dependencies on modules or other configuration (including 196 * other migrations) that must be available before the migration can be 197 * created. 198 * 199 * @see \Drupal\Core\Config\Entity\ConfigDependencyManager 200 * 201 * @var array 202 */ 203 protected $dependencies = []; 204 205 /** 206 * The migration plugin manager for loading other migration plugins. 207 * 208 * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface 209 */ 210 protected $migrationPluginManager; 211 212 /** 213 * The source plugin manager. 214 * 215 * @var \Drupal\migrate\Plugin\MigratePluginManager 216 */ 217 protected $sourcePluginManager; 218 219 /** 220 * The process plugin manager. 221 * 222 * @var \Drupal\migrate\Plugin\MigratePluginManager 223 */ 224 protected $processPluginManager; 225 226 /** 227 * The destination plugin manager. 228 * 229 * @var \Drupal\migrate\Plugin\MigrateDestinationPluginManager 230 */ 231 protected $destinationPluginManager; 232 233 /** 234 * The ID map plugin manager. 235 * 236 * @var \Drupal\migrate\Plugin\MigratePluginManager 237 */ 238 protected $idMapPluginManager; 239 240 /** 241 * Labels corresponding to each defined status. 242 * 243 * @var array 244 */ 245 protected $statusLabels = [ 246 self::STATUS_IDLE => 'Idle', 247 self::STATUS_IMPORTING => 'Importing', 248 self::STATUS_ROLLING_BACK => 'Rolling back', 249 self::STATUS_STOPPING => 'Stopping', 250 self::STATUS_DISABLED => 'Disabled', 251 ]; 252 253 /** 254 * Constructs a Migration. 255 * 256 * @param array $configuration 257 * Plugin configuration. 258 * @param string $plugin_id 259 * The plugin ID. 260 * @param mixed $plugin_definition 261 * The plugin definition. 262 * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager 263 * The migration plugin manager. 264 * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $source_plugin_manager 265 * The source migration plugin manager. 266 * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $process_plugin_manager 267 * The process migration plugin manager. 268 * @param \Drupal\migrate\Plugin\MigrateDestinationPluginManager $destination_plugin_manager 269 * The destination migration plugin manager. 270 * @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $idmap_plugin_manager 271 * The ID map migration plugin manager. 272 */ 273 public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManagerInterface $source_plugin_manager, MigratePluginManagerInterface $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManagerInterface $idmap_plugin_manager) { 274 parent::__construct($configuration, $plugin_id, $plugin_definition); 275 $this->migrationPluginManager = $migration_plugin_manager; 276 $this->sourcePluginManager = $source_plugin_manager; 277 $this->processPluginManager = $process_plugin_manager; 278 $this->destinationPluginManager = $destination_plugin_manager; 279 $this->idMapPluginManager = $idmap_plugin_manager; 280 281 foreach (NestedArray::mergeDeepArray([$plugin_definition, $configuration], TRUE) as $key => $value) { 282 $this->$key = $value; 283 } 284 } 285 286 /** 287 * {@inheritdoc} 288 */ 289 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 290 return new static( 291 $configuration, 292 $plugin_id, 293 $plugin_definition, 294 $container->get('plugin.manager.migration'), 295 $container->get('plugin.manager.migrate.source'), 296 $container->get('plugin.manager.migrate.process'), 297 $container->get('plugin.manager.migrate.destination'), 298 $container->get('plugin.manager.migrate.id_map') 299 ); 300 } 301 302 /** 303 * {@inheritdoc} 304 */ 305 public function id() { 306 return $this->pluginId; 307 } 308 309 /** 310 * {@inheritdoc} 311 */ 312 public function label() { 313 return $this->label; 314 } 315 316 /** 317 * Gets any arbitrary property's value. 318 * 319 * @param string $property 320 * The property to retrieve. 321 * 322 * @return mixed 323 * The value for that property, or NULL if the property does not exist. 324 * 325 * @deprecated in drupal:8.1.0 and is removed from drupal:9.0.0. Use 326 * more specific getters instead. 327 * 328 * @see https://www.drupal.org/node/2873795 329 */ 330 public function get($property) { 331 @trigger_error('\Drupal\migrate\Plugin\Migration::get() is deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.x. Use more specific getters instead. See https://www.drupal.org/node/2873795', E_USER_DEPRECATED); 332 return isset($this->$property) ? $this->$property : NULL; 333 } 334 335 /** 336 * Retrieves the ID map plugin. 337 * 338 * @return \Drupal\migrate\Plugin\MigrateIdMapInterface 339 * The ID map plugin. 340 */ 341 public function getIdMapPlugin() { 342 return $this->idMapPlugin; 343 } 344 345 /** 346 * {@inheritdoc} 347 */ 348 public function getSourcePlugin() { 349 if (!isset($this->sourcePlugin)) { 350 $this->sourcePlugin = $this->sourcePluginManager->createInstance($this->source['plugin'], $this->source, $this); 351 } 352 return $this->sourcePlugin; 353 } 354 355 /** 356 * {@inheritdoc} 357 */ 358 public function getProcessPlugins(array $process = NULL) { 359 if (!isset($process)) { 360 $process = $this->getProcess(); 361 } 362 $index = serialize($process); 363 if (!isset($this->processPlugins[$index])) { 364 $this->processPlugins[$index] = []; 365 foreach ($this->getProcessNormalized($process) as $property => $configurations) { 366 $this->processPlugins[$index][$property] = []; 367 if (!is_array($configurations) && !$this->processPlugins[$index][$property]) { 368 throw new MigrateException(sprintf("Process configuration for '$property' must be an array", $property)); 369 } 370 foreach ($configurations as $configuration) { 371 if (isset($configuration['source'])) { 372 $this->processPlugins[$index][$property][] = $this->processPluginManager->createInstance('get', $configuration, $this); 373 } 374 // Get is already handled. 375 if ($configuration['plugin'] != 'get') { 376 $this->processPlugins[$index][$property][] = $this->processPluginManager->createInstance($configuration['plugin'], $configuration, $this); 377 } 378 if (!$this->processPlugins[$index][$property]) { 379 throw new MigrateException("Invalid process configuration for $property"); 380 } 381 } 382 } 383 } 384 return $this->processPlugins[$index]; 385 } 386 387 /** 388 * Resolve shorthands into a list of plugin configurations. 389 * 390 * @param array $process 391 * A process configuration array. 392 * 393 * @return array 394 * The normalized process configuration. 395 */ 396 protected function getProcessNormalized(array $process) { 397 $normalized_configurations = []; 398 foreach ($process as $destination => $configuration) { 399 if (is_string($configuration)) { 400 $configuration = [ 401 'plugin' => 'get', 402 'source' => $configuration, 403 ]; 404 } 405 if (isset($configuration['plugin'])) { 406 $configuration = [$configuration]; 407 } 408 if (!is_array($configuration)) { 409 $migration_id = $this->getPluginId(); 410 throw new MigrateException("Invalid process for destination '$destination' in migration '$migration_id'"); 411 } 412 $normalized_configurations[$destination] = $configuration; 413 } 414 return $normalized_configurations; 415 } 416 417 /** 418 * {@inheritdoc} 419 */ 420 public function getDestinationPlugin($stub_being_requested = FALSE) { 421 if ($stub_being_requested && !empty($this->destination['no_stub'])) { 422 throw new MigrateSkipRowException('Stub requested but not made because no_stub configuration is set.'); 423 } 424 if (!isset($this->destinationPlugin)) { 425 $this->destinationPlugin = $this->destinationPluginManager->createInstance($this->destination['plugin'], $this->destination, $this); 426 } 427 return $this->destinationPlugin; 428 } 429 430 /** 431 * {@inheritdoc} 432 */ 433 public function getIdMap() { 434 if (!isset($this->idMapPlugin)) { 435 $configuration = $this->idMap; 436 $plugin = isset($configuration['plugin']) ? $configuration['plugin'] : 'sql'; 437 $this->idMapPlugin = $this->idMapPluginManager->createInstance($plugin, $configuration, $this); 438 } 439 return $this->idMapPlugin; 440 } 441 442 /** 443 * {@inheritdoc} 444 */ 445 public function checkRequirements() { 446 // Check whether the current migration source and destination plugin 447 // requirements are met or not. 448 if ($this->getSourcePlugin() instanceof RequirementsInterface) { 449 $this->getSourcePlugin()->checkRequirements(); 450 } 451 if ($this->getDestinationPlugin() instanceof RequirementsInterface) { 452 $this->getDestinationPlugin()->checkRequirements(); 453 } 454 455 if (empty($this->requirements)) { 456 // There are no requirements to check. 457 return; 458 } 459 /** @var \Drupal\migrate\Plugin\MigrationInterface[] $required_migrations */ 460 $required_migrations = $this->getMigrationPluginManager()->createInstances($this->requirements); 461 462 $missing_migrations = array_diff($this->requirements, array_keys($required_migrations)); 463 // Check if the dependencies are in good shape. 464 foreach ($required_migrations as $migration_id => $required_migration) { 465 if (!$required_migration->allRowsProcessed()) { 466 $missing_migrations[] = $migration_id; 467 } 468 } 469 if ($missing_migrations) { 470 throw new RequirementsException('Missing migrations ' . implode(', ', $missing_migrations) . '.', ['requirements' => $missing_migrations]); 471 } 472 } 473 474 /** 475 * Gets the migration plugin manager. 476 * 477 * @return \Drupal\migrate\Plugin\MigratePluginManager 478 * The plugin manager. 479 */ 480 protected function getMigrationPluginManager() { 481 return $this->migrationPluginManager; 482 } 483 484 /** 485 * {@inheritdoc} 486 */ 487 public function setStatus($status) { 488 \Drupal::keyValue('migrate_status')->set($this->id(), $status); 489 } 490 491 /** 492 * {@inheritdoc} 493 */ 494 public function getStatus() { 495 return \Drupal::keyValue('migrate_status')->get($this->id(), static::STATUS_IDLE); 496 } 497 498 /** 499 * {@inheritdoc} 500 */ 501 public function getStatusLabel() { 502 $status = $this->getStatus(); 503 if (isset($this->statusLabels[$status])) { 504 return $this->statusLabels[$status]; 505 } 506 else { 507 return ''; 508 } 509 } 510 511 /** 512 * {@inheritdoc} 513 */ 514 public function getInterruptionResult() { 515 return \Drupal::keyValue('migrate_interruption_result')->get($this->id(), static::RESULT_INCOMPLETE); 516 } 517 518 /** 519 * {@inheritdoc} 520 */ 521 public function clearInterruptionResult() { 522 \Drupal::keyValue('migrate_interruption_result')->delete($this->id()); 523 } 524 525 /** 526 * {@inheritdoc} 527 */ 528 public function interruptMigration($result) { 529 $this->setStatus(MigrationInterface::STATUS_STOPPING); 530 \Drupal::keyValue('migrate_interruption_result')->set($this->id(), $result); 531 } 532 533 /** 534 * {@inheritdoc} 535 */ 536 public function allRowsProcessed() { 537 $source_count = $this->getSourcePlugin()->count(); 538 // If the source is uncountable, we have no way of knowing if it's 539 // complete, so stipulate that it is. 540 if ($source_count < 0) { 541 return TRUE; 542 } 543 $processed_count = $this->getIdMap()->processedCount(); 544 // We don't use == because in some circumstances (like unresolved stubs 545 // being created), the processed count may be higher than the available 546 // source rows. 547 return $source_count <= $processed_count; 548 } 549 550 /** 551 * {@inheritdoc} 552 */ 553 public function set($property_name, $value) { 554 if ($property_name == 'source') { 555 // Invalidate the source plugin. 556 unset($this->sourcePlugin); 557 } 558 elseif ($property_name === 'destination') { 559 // Invalidate the destination plugin. 560 unset($this->destinationPlugin); 561 } 562 $this->{$property_name} = $value; 563 return $this; 564 } 565 566 /** 567 * {@inheritdoc} 568 */ 569 public function getProcess() { 570 return $this->getProcessNormalized($this->process); 571 } 572 573 /** 574 * {@inheritdoc} 575 */ 576 public function setProcess(array $process) { 577 $this->process = $process; 578 return $this; 579 } 580 581 /** 582 * {@inheritdoc} 583 */ 584 public function setProcessOfProperty($property, $process_of_property) { 585 $this->process[$property] = $process_of_property; 586 return $this; 587 } 588 589 /** 590 * {@inheritdoc} 591 */ 592 public function mergeProcessOfProperty($property, array $process_of_property) { 593 // If we already have a process value then merge the incoming process array 594 // otherwise simply set it. 595 $current_process = $this->getProcess(); 596 if (isset($current_process[$property])) { 597 $this->process = NestedArray::mergeDeepArray([$current_process, $this->getProcessNormalized([$property => $process_of_property])], TRUE); 598 } 599 else { 600 $this->setProcessOfProperty($property, $process_of_property); 601 } 602 603 return $this; 604 } 605 606 /** 607 * {@inheritdoc} 608 */ 609 public function isTrackLastImported() { 610 return $this->trackLastImported; 611 } 612 613 /** 614 * {@inheritdoc} 615 */ 616 public function setTrackLastImported($track_last_imported) { 617 $this->trackLastImported = (bool) $track_last_imported; 618 return $this; 619 } 620 621 /** 622 * {@inheritdoc} 623 */ 624 public function getMigrationDependencies() { 625 $this->migration_dependencies = ($this->migration_dependencies ?: []) + ['required' => [], 'optional' => []]; 626 if (count($this->migration_dependencies) !== 2 || !is_array($this->migration_dependencies['required']) || !is_array($this->migration_dependencies['optional'])) { 627 throw new InvalidPluginDefinitionException($this->id(), "Invalid migration dependencies configuration for migration {$this->id()}"); 628 } 629 $this->migration_dependencies['optional'] = array_unique(array_merge($this->migration_dependencies['optional'], $this->findMigrationDependencies($this->process))); 630 return $this->migration_dependencies; 631 } 632 633 /** 634 * Find migration dependencies from migration_lookup and sub_process plugins. 635 * 636 * @param array $process 637 * A process configuration array. 638 * 639 * @return array 640 * The migration dependencies. 641 */ 642 protected function findMigrationDependencies($process) { 643 $return = []; 644 foreach ($this->getProcessNormalized($process) as $process_pipeline) { 645 foreach ($process_pipeline as $plugin_configuration) { 646 if (in_array($plugin_configuration['plugin'], ['migration', 'migration_lookup'], TRUE)) { 647 $return = array_merge($return, (array) $plugin_configuration['migration']); 648 } 649 if (in_array($plugin_configuration['plugin'], ['iterator', 'sub_process'], TRUE)) { 650 $return = array_merge($return, $this->findMigrationDependencies($plugin_configuration['process'])); 651 } 652 } 653 } 654 return $return; 655 } 656 657 /** 658 * {@inheritdoc} 659 */ 660 public function getPluginDefinition() { 661 $definition = []; 662 // While normal plugins do not change their definitions on the fly, this 663 // one does so accommodate for that. 664 foreach (parent::getPluginDefinition() as $key => $value) { 665 $definition[$key] = isset($this->$key) ? $this->$key : $value; 666 } 667 return $definition; 668 } 669 670 /** 671 * {@inheritdoc} 672 */ 673 public function getDestinationConfiguration() { 674 return $this->destination; 675 } 676 677 /** 678 * {@inheritdoc} 679 */ 680 public function getSourceConfiguration() { 681 return $this->source; 682 } 683 684 /** 685 * {@inheritdoc} 686 */ 687 public function getTrackLastImported() { 688 return $this->trackLastImported; 689 } 690 691 /** 692 * {@inheritdoc} 693 */ 694 public function getDestinationIds() { 695 return $this->destinationIds; 696 } 697 698 /** 699 * {@inheritdoc} 700 */ 701 public function getMigrationTags() { 702 return $this->migration_tags; 703 } 704 705 /** 706 * {@inheritdoc} 707 */ 708 public function isAuditable() { 709 return (bool) $this->audit; 710 } 711 712} 713