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