diff --git a/core/modules/block_content/migration_templates/block_content_body_field.yml b/core/modules/block_content/migration_templates/block_content_body_field.yml index 41484f8..b51d032 100644 --- a/core/modules/block_content/migration_templates/block_content_body_field.yml +++ b/core/modules/block_content/migration_templates/block_content_body_field.yml @@ -30,3 +30,6 @@ destination: migration_dependencies: required: - block_content_type +provider: + - block_content + - migrate_drupal diff --git a/core/modules/block_content/migration_templates/block_content_type.yml b/core/modules/block_content/migration_templates/block_content_type.yml index 7b77ba5..bc75eea 100644 --- a/core/modules/block_content/migration_templates/block_content_type.yml +++ b/core/modules/block_content/migration_templates/block_content_type.yml @@ -17,3 +17,6 @@ process: label: label destination: plugin: entity:block_content_type +provider: + - block_content + - migrate_drupal diff --git a/core/modules/migrate/migrate.services.yml b/core/modules/migrate/migrate.services.yml index da43b38..c9a6457 100644 --- a/core/modules/migrate/migrate.services.yml +++ b/core/modules/migrate/migrate.services.yml @@ -6,8 +6,8 @@ services: factory: cache_factory:get arguments: [migrate] plugin.manager.migrate.source: - class: Drupal\migrate\Plugin\MigratePluginManager - arguments: [source, '@container.namespaces', '@cache.discovery', '@module_handler', 'Drupal\migrate\Annotation\MigrateSource'] + class: Drupal\migrate\Plugin\MigrateSourcePluginManager + arguments: [source, '@container.namespaces', '@cache.discovery', '@module_handler', '@class_loader'] plugin.manager.migrate.process: class: Drupal\migrate\Plugin\MigratePluginManager arguments: [process, '@container.namespaces', '@cache.discovery', '@module_handler', 'Drupal\migrate\Annotation\MigrateProcessPlugin'] diff --git a/core/modules/migrate/src/Annotation/MigrateSource.php b/core/modules/migrate/src/Annotation/MigrateSource.php index a73f7a5..95a2431 100644 --- a/core/modules/migrate/src/Annotation/MigrateSource.php +++ b/core/modules/migrate/src/Annotation/MigrateSource.php @@ -24,7 +24,7 @@ * * @Annotation */ -class MigrateSource extends Plugin { +class MigrateSource extends Plugin implements MultipleProviderAnnotationInterface { /** * A unique identifier for the process plugin. @@ -66,4 +66,34 @@ class MigrateSource extends Plugin { */ public $minimum_version; + /** + * {@inheritdoc} + */ + public function getProvider() { + if (isset($this->definition['provider'])) { + return is_array($this->definition['provider']) ? reset($this->definition['provider']) : $this->definition['provider']; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getProviders() { + if (isset($this->definition['provider'])) { + // Ensure that we return an array even if + // \Drupal\Component\Annotation\AnnotationInterface::setProvider() has + // been called. + return (array) $this->definition['provider']; + } + return []; + } + + /** + * {@inheritdoc} + */ + public function setProviders(array $providers) { + $this->definition['provider'] = $providers; + } + } diff --git a/core/modules/migrate/src/Annotation/MultipleProviderAnnotationInterface.php b/core/modules/migrate/src/Annotation/MultipleProviderAnnotationInterface.php new file mode 100644 index 0000000..3930c87 --- /dev/null +++ b/core/modules/migrate/src/Annotation/MultipleProviderAnnotationInterface.php @@ -0,0 +1,41 @@ +finder = $class_loader; + } + + + /** + * {@inheritdoc} + */ + protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class, BaseStaticReflectionParser $parser = NULL) { + if (!($annotation instanceof MultipleProviderAnnotationInterface)) { + throw new \LogicException('AnnotatedClassDiscoveryAutomatedProviders annotations must implement \Drupal\migrate\Annotation\MultipleProviderAnnotationInterface'); + } + $annotation->setClass($class); + $providers = $annotation->getProviders(); + // Loop through all the parent classes and add their providers (which we + // infer by parsing their use statements) to the $providers array. + do { + $new_providers = array_map([$this, 'getProviderFromNamespace'], $parser->getUseStatements()); + $providers = array_merge($providers, $new_providers); + } while ($parser = StaticReflectionParser::getParentParser($parser, $this->finder)); + $providers = array_unique(array_filter($providers, function ($provider) { + return $provider && $provider !== 'component'; + })); + $annotation->setProviders($providers); + } + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + $definitions = array(); + + $reader = $this->getAnnotationReader(); + + // Clear the annotation loaders of any previous annotation classes. + AnnotationRegistry::reset(); + // Register the namespaces of classes that can be used for annotations. + AnnotationRegistry::registerLoader('class_exists'); + + // Search for classes within all PSR-0 namespace locations. + foreach ($this->getPluginNamespaces() as $namespace => $dirs) { + foreach ($dirs as $dir) { + if (file_exists($dir)) { + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS) + ); + foreach ($iterator as $fileinfo) { + if ($fileinfo->getExtension() == 'php') { + if ($cached = $this->fileCache->get($fileinfo->getPathName())) { + if (isset($cached['id'])) { + // Explicitly unserialize this to create a new object instance. + $definitions[$cached['id']] = unserialize($cached['content']); + } + continue; + } + + $sub_path = $iterator->getSubIterator()->getSubPath(); + $sub_path = $sub_path ? str_replace(DIRECTORY_SEPARATOR, '\\', $sub_path) . '\\' : ''; + $class = $namespace . '\\' . $sub_path . $fileinfo->getBasename('.php'); + + // The filename is already known, so there is no need to find the + // file. However, StaticReflectionParser needs a finder, so use a + // mock version. + $finder = MockFileFinder::create($fileinfo->getPathName()); + $parser = new BaseStaticReflectionParser($class, $finder, FALSE); + + /** @var $annotation \Drupal\Component\Annotation\AnnotationInterface */ + if ($annotation = $reader->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) { + $this->prepareAnnotationDefinition($annotation, $class, $parser); + + $id = $annotation->getId(); + $content = $annotation->get(); + $definitions[$id] = $content; + // Explicitly serialize this to create a new object instance. + $this->fileCache->set($fileinfo->getPathName(), ['id' => $id, 'content' => serialize($content)]); + } + else { + // Store a NULL object, so the file is not reparsed again. + $this->fileCache->set($fileinfo->getPathName(), [NULL]); + } + } + } + } + } + } + + // Don't let annotation loaders pile up. + AnnotationRegistry::reset(); + + return $definitions; + } + +} diff --git a/core/modules/migrate/src/Plugin/Discovery/ProviderFilterDecorator.php b/core/modules/migrate/src/Plugin/Discovery/ProviderFilterDecorator.php new file mode 100644 index 0000000..8a2dfb8 --- /dev/null +++ b/core/modules/migrate/src/Plugin/Discovery/ProviderFilterDecorator.php @@ -0,0 +1,99 @@ +decorated = $decorated; + $this->providerExists = $provider_exists; + } + + /** + * Removes plugin definitions with non-existing providers. + * + * @param mixed[] $definitions + * An array of plugin definitions (empty array if no definitions were + * found). Keys are plugin IDs. + * @param callable $provider_exists + * A callable, gets passed a provider name, should return TRUE if the + * provider exists and FALSE if not. + * + * @return array|\mixed[] $definitions + * An array of plugin definitions. If a definition is an array and has a + * provider key that provider is guaranteed to exist. + */ + public static function filterDefinitions(array $definitions, callable $provider_exists) { + // Besides what the caller accepts, we also accept core or component. + $provider_exists = function ($provider) use ($provider_exists) { + return in_array($provider, ['core', 'component']) || $provider_exists($provider); + }; + return array_filter($definitions, function ($definition) use ($provider_exists) { + // Plugin definitions can be objects (for example, Typed Data) those will + // become empty array here and cause no problems. + $definition = (array) $definition + ['provider' => []]; + // There can be one or many providers, handle them as multiple always. + $providers = (array) $definition['provider']; + return count($providers) == count(array_filter($providers, $provider_exists)); + }); + } + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + return static::filterDefinitions($this->decorated->getDefinitions(), $this->providerExists); + } + + /** + * Passes through all unknown calls onto the decorated object. + * + * @param string $method + * The method to call on the decorated object. + * @param array $args + * Call arguments. + * + * @return mixed + * The return value from the method on the decorated object. + */ + public function __call($method, array $args) { + return call_user_func_array([$this->decorated, $method], $args); + } + +} diff --git a/core/modules/migrate/src/Plugin/Discovery/StaticReflectionParser.php b/core/modules/migrate/src/Plugin/Discovery/StaticReflectionParser.php new file mode 100644 index 0000000..790f8b1 --- /dev/null +++ b/core/modules/migrate/src/Plugin/Discovery/StaticReflectionParser.php @@ -0,0 +1,37 @@ +parentClassName) { + return new static($parser->parentClassName, $finder, $parser->classAnnotationOptimize); + } + } + +} diff --git a/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php b/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php new file mode 100644 index 0000000..34ac0c5 --- /dev/null +++ b/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php @@ -0,0 +1,83 @@ +classLoader = $class_loader; + } + + /** + * {@inheritdoc} + */ + protected function getDiscovery() { + if (!$this->discovery) { + $discovery = new AnnotatedClassDiscoveryAutomatedProviders($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces, $this->classLoader); + $this->discovery = new ContainerDerivativeDiscoveryDecorator($discovery); + } + return $this->discovery; + } + + /** + * 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); + }); + } + +} diff --git a/core/modules/migrate/src/Plugin/MigrationPluginManager.php b/core/modules/migrate/src/Plugin/MigrationPluginManager.php index d082c11..690e681 100644 --- a/core/modules/migrate/src/Plugin/MigrationPluginManager.php +++ b/core/modules/migrate/src/Plugin/MigrationPluginManager.php @@ -9,6 +9,7 @@ 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; @@ -68,7 +69,15 @@ protected function getDiscovery() { }, $this->moduleHandler->getModuleDirectories()); $yaml_discovery = new YamlDirectoryDiscovery($directories, 'migrate'); - $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery); + // 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; } @@ -228,4 +237,25 @@ public function createStubMigration(array $definition) { 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); + }); + } + } diff --git a/core/modules/migrate/src/Plugin/NoSourcePluginDecorator.php b/core/modules/migrate/src/Plugin/NoSourcePluginDecorator.php new file mode 100644 index 0000000..aa2121e --- /dev/null +++ b/core/modules/migrate/src/Plugin/NoSourcePluginDecorator.php @@ -0,0 +1,58 @@ +decorated = $decorated; + } + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + /** @var \Drupal\Component\Plugin\PluginManagerInterface $source_plugin_manager */ + $source_plugin_manager = \Drupal::service('plugin.manager.migrate.source'); + return array_filter($this->decorated->getDefinitions(), function (array $definition) use ($source_plugin_manager) { + return $source_plugin_manager->hasDefinition($definition['source']['plugin']); + }); + } + + /** + * Passes through all unknown calls onto the decorated object. + * + * @param string $method + * The method to call on the decorated object. + * @param array $args + * Call arguments. + * + * @return mixed + * The return value from the method on the decorated object. + */ + public function __call($method, array $args) { + return call_user_func_array([$this->decorated, $method], $args); + } + +} diff --git a/core/modules/migrate/tests/src/Kernel/MigrationTest.php b/core/modules/migrate/tests/src/Kernel/MigrationTest.php index 9ff9f80..5fe56ec 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrationTest.php +++ b/core/modules/migrate/tests/src/Kernel/MigrationTest.php @@ -34,8 +34,8 @@ public function testSetInvalidation() { $this->assertEqual('entity:entity_view_mode', $migration->getDestinationPlugin()->getPluginId()); // Test the source plugin is invalidated. - $migration->set('source', ['plugin' => 'd6_field']); - $this->assertEqual('d6_field', $migration->getSourcePlugin()->getPluginId()); + $migration->set('source', ['plugin' => 'embedded_data', 'data_rows' => [], 'ids' => []]); + $this->assertEqual('embedded_data', $migration->getSourcePlugin()->getPluginId()); // Test the destination plugin is invalidated. $migration->set('destination', ['plugin' => 'null']); diff --git a/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php b/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php new file mode 100644 index 0000000..f732d9f --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php @@ -0,0 +1,106 @@ +container->get('plugin.manager.migration')->getDefinitions(); + // All the plugins provided by core depend on migrate_drupal. + $this->assertEmpty($migration_plugins); + + // Enable a module that provides migrations that do not depend on + // migrate_drupal. + $this->enableModules(['migrate_external_translated_test']); + $migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions(); + // All the plugins provided by migrate_external_translated_test do not + // depend on migrate_drupal. + $this::assertArrayHasKey('external_translated_test_node', $migration_plugins); + $this::assertArrayHasKey('external_translated_test_node_translation', $migration_plugins); + + // Disable the test module and the list should be empty again. + $this->disableModules(['migrate_external_translated_test']); + $migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions(); + // All the plugins provided by core depend on migrate_drupal. + $this->assertEmpty($migration_plugins); + + // Enable migrate_drupal to test that the plugins can now be discovered. + $this->enableModules(['migrate_drupal']); + // Set up a migrate database connection so that plugin discovery works. + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('migrate'); + if ($connection_info) { + Database::renameConnection('migrate', 'simpletest_original_migrate'); + } + $connection_info = Database::getConnectionInfo('default'); + foreach ($connection_info as $target => $value) { + $prefix = is_array($value['prefix']) ? $value['prefix']['default'] : $value['prefix']; + // Simpletest uses 7 character prefixes at most so this can't cause + // collisions. + $connection_info[$target]['prefix']['default'] = $prefix . '0'; + + // Add the original simpletest prefix so SQLite can attach its database. + // @see \Drupal\Core\Database\Driver\sqlite\Connection::init() + $connection_info[$target]['prefix'][$value['prefix']['default']] = $value['prefix']['default']; + } + Database::addConnectionInfo('migrate', 'default', $connection_info['default']); + + $migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions(); + // All the plugins provided by core depend on migrate_drupal. + $this->assertNotEmpty($migration_plugins); + } + +} diff --git a/core/modules/taxonomy/tests/modules/taxonomy_term_stub_test/migrations/taxonomy_term_stub_test.yml b/core/modules/taxonomy/tests/modules/taxonomy_term_stub_test/migrations/taxonomy_term_stub_test.yml index ddca901..ad56fff 100644 --- a/core/modules/taxonomy/tests/modules/taxonomy_term_stub_test/migrations/taxonomy_term_stub_test.yml +++ b/core/modules/taxonomy/tests/modules/taxonomy_term_stub_test/migrations/taxonomy_term_stub_test.yml @@ -27,3 +27,6 @@ destination: migration_dependencies: required: - vocabularies +provider: + - migrate_drupal + - taxonomy