diff --git a/core/lib/Drupal/Component/Annotation/AnnotationInterface.php b/core/lib/Drupal/Component/Annotation/AnnotationInterface.php index 1168510..bbb86cd 100644 --- a/core/lib/Drupal/Component/Annotation/AnnotationInterface.php +++ b/core/lib/Drupal/Component/Annotation/AnnotationInterface.php @@ -20,9 +20,9 @@ public function get(); public function getProvider(); /** - * Sets the name of the provider of the annotated class. + * Sets the name of the provider(s) of the annotated class. * - * @param string $provider + * @param array|string $provider */ public function setProvider($provider); diff --git a/core/lib/Drupal/Component/Annotation/Plugin.php b/core/lib/Drupal/Component/Annotation/Plugin.php index 29f3675..4df1f3c 100644 --- a/core/lib/Drupal/Component/Annotation/Plugin.php +++ b/core/lib/Drupal/Component/Annotation/Plugin.php @@ -78,7 +78,10 @@ public function get() { * {@inheritdoc} */ public function getProvider() { - return isset($this->definition['provider']) ? $this->definition['provider'] : FALSE; + if (isset($this->definition['provider'])) { + return is_array($this->definition['provider']) ? reset($this->definition['provider']) : $this->definition['provider']; + } + return FALSE; } /** diff --git a/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php b/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php index e564fd0..a35b22c 100644 --- a/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php +++ b/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php @@ -19,6 +19,11 @@ class AnnotatedClassDiscovery implements DiscoveryInterface { use DiscoveryTrait; /** + * Whether the parser should return only class annotations. + */ + const CLASS_ANNOTATION_OPTIMIZE = TRUE; + + /** * The namespaces within which to find plugin classes. * * @var string[] @@ -138,11 +143,11 @@ public function getDefinitions() { // file. However, StaticReflectionParser needs a finder, so use a // mock version. $finder = MockFileFinder::create($fileinfo->getPathName()); - $parser = new StaticReflectionParser($class, $finder, TRUE); + $parser = new StaticReflectionParser($class, $finder, static::CLASS_ANNOTATION_OPTIMIZE); /** @var $annotation \Drupal\Component\Annotation\AnnotationInterface */ if ($annotation = $reader->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) { - $this->prepareAnnotationDefinition($annotation, $class); + $this->prepareAnnotationDefinition($annotation, $class, $parser); $id = $annotation->getId(); $content = $annotation->get(); @@ -173,8 +178,10 @@ public function getDefinitions() { * The annotation derived from the plugin. * @param string $class * The class used for the plugin. + * @param \Doctrine\Common\Reflection\StaticReflectionParser $parser + * The static reflection parser. */ - protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class) { + protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class, StaticReflectionParser $parser) { $annotation->setClass($class); } diff --git a/core/lib/Drupal/Component/Plugin/Discovery/StaticReflectionParser.php b/core/lib/Drupal/Component/Plugin/Discovery/StaticReflectionParser.php new file mode 100644 index 0000000..05aad55 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Discovery/StaticReflectionParser.php @@ -0,0 +1,32 @@ +parentClassName) { + return new static($parser->parentClassName, $finder, $parser->classAnnotationOptimize); + } + } + +} diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php index fa706a4..a84734f 100644 --- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php +++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php @@ -14,6 +14,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; +use Drupal\Core\Plugin\Discovery\ProviderFilterDecorator; use Drupal\Core\Plugin\Factory\ContainerFactory; /** @@ -288,19 +289,9 @@ protected function findDefinitions() { $this->processDefinition($definition, $plugin_id); } $this->alterDefinitions($definitions); - // If this plugin was provided by a module that does not exist, remove the - // plugin definition. - foreach ($definitions as $plugin_id => $plugin_definition) { - // If the plugin definition is an object, attempt to convert it to an - // array, if that is not possible, skip further processing. - if (is_object($plugin_definition) && !($plugin_definition = (array) $plugin_definition)) { - continue; - } - if (isset($plugin_definition['provider']) && !in_array($plugin_definition['provider'], array('core', 'component')) && !$this->providerExists($plugin_definition['provider'])) { - unset($definitions[$plugin_id]); - } - } - return $definitions; + return ProviderFilterDecorator::filterDefinitions($definitions, function ($provider) { + return $this->providerExists($provider); + }); } /** diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php index 770308d..f49dced 100644 --- a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php +++ b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Plugin\Discovery; +use Doctrine\Common\Reflection\StaticReflectionParser; use Drupal\Component\Annotation\AnnotationInterface; use Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery as ComponentAnnotatedClassDiscovery; use Drupal\Component\Utility\Unicode; @@ -82,8 +83,8 @@ protected function getAnnotationReader() { /** * {@inheritdoc} */ - protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class) { - parent::prepareAnnotationDefinition($annotation, $class); + protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class, StaticReflectionParser $parser) { + parent::prepareAnnotationDefinition($annotation, $class, $parser); if (!$annotation->getProvider()) { $annotation->setProvider($this->getProviderFromNamespace($class)); diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscoveryAutomatedProviders.php b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscoveryAutomatedProviders.php new file mode 100644 index 0000000..6530d51 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscoveryAutomatedProviders.php @@ -0,0 +1,69 @@ +finder = $class_loader; + } + + /** + * {@inheritdoc} + */ + protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class, BaseStaticReflectionParser $parser) { + parent::prepareAnnotationDefinition($annotation, $class, $parser); + $providers = (array) $annotation->getProvider(); + // 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)); + $annotation->setProvider(array_unique(array_filter($providers))); + } + +} diff --git a/core/lib/Drupal/Core/Plugin/Discovery/ProviderFilterDecorator.php b/core/lib/Drupal/Core/Plugin/Discovery/ProviderFilterDecorator.php new file mode 100644 index 0000000..8d8c02d --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Discovery/ProviderFilterDecorator.php @@ -0,0 +1,94 @@ +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/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/Plugin/MigrateSourcePluginManager.php b/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php new file mode 100644 index 0000000..e039b35 --- /dev/null +++ b/core/modules/migrate/src/Plugin/MigrateSourcePluginManager.php @@ -0,0 +1,63 @@ +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; + } + +} + diff --git a/core/modules/migrate/src/Plugin/MigrationPluginManager.php b/core/modules/migrate/src/Plugin/MigrationPluginManager.php index d082c11..78f9e2f 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\Core\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; } 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