summaryrefslogtreecommitdiffstatshomepage
path: root/core/modules/migrate/src/Plugin/MigrationPluginManager.php
blob: aa77e24b068e9e67024a338691ffdd7bea913ff7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
<?php

namespace Drupal\migrate\Plugin;

use Drupal\Component\Graph\Graph;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\migrate\Plugin\Discovery\ProviderFilterDecorator;
use Drupal\Core\Plugin\Discovery\YamlDirectoryDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\migrate\MigrateBuildDependencyInterface;

/**
 * Plugin manager for migration plugins.
 */
class MigrationPluginManager extends DefaultPluginManager implements MigrationPluginManagerInterface, MigrateBuildDependencyInterface {

  /**
   * Provides default values for migrations.
   *
   * @var array
   */
  protected $defaults = [
    'class' => '\Drupal\migrate\Plugin\Migration',
  ];

  /**
   * The interface the plugins should implement.
   *
   * @var string
   */
  protected $pluginInterface = 'Drupal\migrate\Plugin\MigrationInterface';

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Construct a migration plugin manager.
   *
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   The cache backend for the definitions.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   */
  public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager) {
    $this->factory = new ContainerFactory($this, $this->pluginInterface);
    $this->alterInfo('migration_plugins');
    $this->setCacheBackend($cache_backend, 'migration_plugins');
    $this->moduleHandler = $module_handler;
  }

  /**
   * Gets the plugin discovery.
   *
   * This method overrides DefaultPluginManager::getDiscovery() in order to
   * search for migration configurations in the MODULENAME/migrations
   * directory.
   */
  protected function getDiscovery() {
    if (!isset($this->discovery)) {
      $directories = array_map(function ($directory) {
        return [$directory . '/migrations'];
      }, $this->moduleHandler->getModuleDirectories());

      $yaml_discovery = new YamlDirectoryDiscovery($directories, 'migrate');
      // This gets rid of migrations which try to use a non-existent source
      // plugin. The common case for this is if the source plugin has, or
      // specifies, a non-existent provider.
      $only_with_source_discovery = new NoSourcePluginDecorator($yaml_discovery);
      // This gets rid of migrations with explicit providers set if one of the
      // providers do not exist before we try to use a potentially non-existing
      // deriver. This is a rare case.
      $filtered_discovery = new ProviderFilterDecorator($only_with_source_discovery, [$this->moduleHandler, 'moduleExists']);
      $this->discovery = new ContainerDerivativeDiscoveryDecorator($filtered_discovery);
    }
    return $this->discovery;
  }

  /**
   * {@inheritdoc}
   */
  public function createInstance($plugin_id, array $configuration = []) {
    $instances = $this->createInstances([$plugin_id], [$plugin_id => $configuration]);
    return reset($instances);
  }

  /**
   * {@inheritdoc}
   */
  public function createInstances($migration_id, array $configuration = []) {
    if (empty($migration_id)) {
      $migration_id = array_keys($this->getDefinitions());
    }

    $factory = $this->getFactory();
    $migration_ids = (array) $migration_id;

    // We need to expand any derivative migrations. Derivative migrations are
    // calculated by migration derivers such as D6NodeDeriver. This allows
    // migrations to depend on the base id and then have a dependency on all
    // derivative migrations. For example, d6_comment depends on d6_node but
    // after we've expanded the dependencies it will depend on d6_node:page,
    // d6_node:story and so on, for other derivative migrations.
    $plugin_ids = $this->expandPluginIds($migration_ids);

    $instances = [];
    foreach ($plugin_ids as $plugin_id) {
      $instances[$plugin_id] = $factory->createInstance($plugin_id, $configuration[$plugin_id] ?? []);
    }

    // @todo Remove loop when the ability to call ::getMigrationDependencies()
    //   without expanding plugins is removed.
    foreach ($instances as $migration) {
      $migration->set('migration_dependencies', $migration->getMigrationDependencies());
    }

    // Sort the migrations based on their dependencies.
    return $this->buildDependencyMigration($instances, []);
  }

  /**
   * {@inheritdoc}
   */
  public function createInstancesByTag($tag) {
    $migrations = array_filter($this->getDefinitions(), function ($migration) use ($tag) {
      return !empty($migration['migration_tags']) && in_array($tag, $migration['migration_tags']);
    });
    return $migrations ? $this->createInstances(array_keys($migrations)) : [];
  }

  /**
   * {@inheritdoc}
   */
  public function expandPluginIds(array $migration_ids) {
    $plugin_ids = [];
    $all_ids = array_keys($this->getDefinitions());
    foreach ($migration_ids as $id) {
      $plugin_ids = array_merge($plugin_ids, preg_grep('/^' . preg_quote($id, '/') . PluginBase::DERIVATIVE_SEPARATOR . '/', $all_ids));
      if ($this->hasDefinition($id)) {
        $plugin_ids[] = $id;
      }
    }
    return $plugin_ids;
  }

  /**
   * {@inheritdoc}
   */
  public function buildDependencyMigration(array $migrations, array $dynamic_ids) {
    // Migration dependencies can be optional or required. If an optional
    // dependency does not run, the current migration is still OK to go. Both
    // optional and required dependencies (if run at all) must run before the
    // current migration.
    $dependency_graph = [];
    $required_dependency_graph = [];
    $have_optional = FALSE;
    foreach ($migrations as $migration) {
      /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
      $id = $migration->id();
      $requirements[$id] = [];
      $dependency_graph[$id]['edges'] = [];
      $migration_dependencies = $migration->getMigrationDependencies();

      if (isset($migration_dependencies['required'])) {
        foreach ($migration_dependencies['required'] as $dependency) {
          if (!isset($dynamic_ids[$dependency])) {
            $this->addDependency($required_dependency_graph, $id, $dependency, $dynamic_ids);
          }
          $this->addDependency($dependency_graph, $id, $dependency, $dynamic_ids);
        }
      }
      if (!empty($migration_dependencies['optional'])) {
        foreach ($migration_dependencies['optional'] as $dependency) {
          $this->addDependency($dependency_graph, $id, $dependency, $dynamic_ids);
        }
        $have_optional = TRUE;
      }
    }
    $dependency_graph = (new Graph($dependency_graph))->searchAndSort();
    if ($have_optional) {
      $required_dependency_graph = (new Graph($required_dependency_graph))->searchAndSort();
    }
    else {
      $required_dependency_graph = $dependency_graph;
    }
    $weights = [];
    foreach ($migrations as $migration_id => $migration) {
      // Populate a weights array to use with array_multisort() later.
      $weights[] = $dependency_graph[$migration_id]['weight'];
      if (!empty($required_dependency_graph[$migration_id]['paths'])) {
        $migration->set('requirements', $required_dependency_graph[$migration_id]['paths']);
      }
    }
    // Sort weights, labels, and keys in the same order as each other.
    array_multisort(
      // Use the numerical weight as the primary sort.
      $weights, SORT_DESC, SORT_NUMERIC,
      // When migrations have the same weight, sort them alphabetically by ID.
      array_keys($migrations), SORT_ASC, SORT_NATURAL,
      $migrations
    );

    return $migrations;
  }

  /**
   * Add one or more dependencies to a graph.
   *
   * @param array $graph
   *   The graph so far, passed by reference.
   * @param int $id
   *   The migration ID.
   * @param string $dependency
   *   The dependency string.
   * @param array $dynamic_ids
   *   The dynamic ID mapping.
   */
  protected function addDependency(array &$graph, $id, $dependency, $dynamic_ids) {
    $dependencies = $dynamic_ids[$dependency] ?? [$dependency];
    if (!isset($graph[$id]['edges'])) {
      $graph[$id]['edges'] = [];
    }
    $graph[$id]['edges'] += array_combine($dependencies, $dependencies);
  }

  /**
   * {@inheritdoc}
   */
  public function createStubMigration(array $definition) {
    $id = $definition['id'] ?? uniqid();
    return Migration::create(\Drupal::getContainer(), [], $id, $definition);
  }

  /**
   * Finds plugin definitions.
   *
   * @return array
   *   List of definitions to store in cache.
   *
   * @todo This is a temporary solution to the fact that migration source
   *   plugins have more than one provider. This functionality will be moved to
   *   core in https://www.drupal.org/node/2786355.
   */
  protected function findDefinitions() {
    $definitions = $this->getDiscovery()->getDefinitions();
    foreach ($definitions as $plugin_id => &$definition) {
      $this->processDefinition($definition, $plugin_id);
    }
    $this->alterDefinitions($definitions);
    return ProviderFilterDecorator::filterDefinitions($definitions, function ($provider) {
      return $this->providerExists($provider);
    });
  }

}