Issue #3424509 by godotislate, quietone, sorlov, smustgrave, benjifisher, alexpott: Update MigratePluginManager to include both attribute and annotation class

(cherry picked from commit c9d53fd473)
merge-requests/7353/head
Alex Pott 2024-04-04 00:40:51 +01:00
parent fcc813aa47
commit d7f89942a7
No known key found for this signature in database
GPG Key ID: BDA67E7EE836E5CE
37 changed files with 763 additions and 105 deletions

View File

@ -117,7 +117,7 @@ class AttributeDiscoveryWithAnnotations extends AttributeClassDiscovery {
* @see \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition()
* @see \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition()
*/
private function prepareAnnotationDefinition(AnnotationInterface $annotation, string $class): void {
protected function prepareAnnotationDefinition(AnnotationInterface $annotation, string $class): void {
$annotation->setClass($class);
if (!$annotation->getProvider()) {
$annotation->setProvider($this->getProviderFromNamespace($class));
@ -136,7 +136,7 @@ class AttributeDiscoveryWithAnnotations extends AttributeClassDiscovery {
* @see \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery::getAnnotationReader()
* @see \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery::getAnnotationReader()
*/
private function getAnnotationReader() : SimpleAnnotationReader {
protected function getAnnotationReader() : SimpleAnnotationReader {
if (!isset($this->annotationReader)) {
$this->annotationReader = new SimpleAnnotationReader();

View File

@ -3,15 +3,14 @@
namespace Drupal\book\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "book",
* provider = "book"
* )
* Provides migrate destination plugin for Book content.
*/
#[MigrateDestination('book')]
class Book extends EntityContentBase {
/**

View File

@ -6,25 +6,25 @@ use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
// cspell:ignore todate
/**
* Provides a field plugin for date and time fields.
*
* @MigrateField(
* id = "datetime",
* type_map = {
* "date" = "datetime",
* "datestamp" = "timestamp",
* "datetime" = "datetime",
* },
* core = {6,7},
* source_module = "date",
* destination_module = "datetime"
* )
*/
#[MigrateField(
id: 'datetime',
core: [6, 7],
type_map: [
'date' => 'datetime',
'datestamp' => 'timestamp',
'datetime' => 'datetime',
],
source_module: 'date',
destination_module: 'datetime',
)]
class DateField extends FieldPluginBase {
/**

View File

@ -3,15 +3,15 @@
namespace Drupal\file\Plugin\migrate\destination;
use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
/**
* @MigrateDestination(
* id = "entity:file"
* )
* Provides migrate destination plugin for File entities.
*/
#[MigrateDestination('entity:file')]
class EntityFile extends EntityContentBase {
/**

View File

@ -47,8 +47,8 @@ use Drupal\migrate\Row;
* @section sec_source Migrate API source plugins
* Migrate API source plugins implement
* \Drupal\migrate\Plugin\MigrateSourceInterface and usually extend
* \Drupal\migrate\Plugin\migrate\source\SourcePluginBase. They are annotated
* with \Drupal\migrate\Annotation\MigrateSource annotation and must be in
* \Drupal\migrate\Plugin\migrate\source\SourcePluginBase. They have the
* \Drupal\migrate\Attribute\MigrateSource attribute and must be in
* namespace subdirectory 'Plugin\migrate\source' under the namespace of the
* module that defines them. Migrate API source plugins are managed by the
* \Drupal\migrate\Plugin\MigrateSourcePluginManager class.
@ -59,8 +59,8 @@ use Drupal\migrate\Row;
* @section sec_process Migrate API process plugins
* Migrate API process plugins implement
* \Drupal\migrate\Plugin\MigrateProcessInterface and usually extend
* \Drupal\migrate\ProcessPluginBase. They are annotated with
* \Drupal\migrate\Annotation\MigrateProcessPlugin annotation and must be in
* \Drupal\migrate\ProcessPluginBase. They have the
* \Drupal\migrate\Attribute\MigrateProcess attribute and must be in
* namespace subdirectory 'Plugin\migrate\process' under the namespace of the
* module that defines them. Migrate API process plugins are managed by the
* \Drupal\migrate\Plugin\MigratePluginManager class.
@ -70,12 +70,11 @@ use Drupal\migrate\Row;
* @section sec_destination Migrate API destination plugins
* Migrate API destination plugins implement
* \Drupal\migrate\Plugin\MigrateDestinationInterface and usually extend
* \Drupal\migrate\Plugin\migrate\destination\DestinationBase. They are
* annotated with \Drupal\migrate\Annotation\MigrateDestination annotation and
* must be in namespace subdirectory 'Plugin\migrate\destination' under the
* namespace of the module that defines them. Migrate API destination plugins
* are managed by the \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* class.
* \Drupal\migrate\Plugin\migrate\destination\DestinationBase. They have the
* \Drupal\migrate\Attribute\MigrateDestination attribute and must be in
* namespace subdirectory 'Plugin\migrate\destination' under the namespace of
* the module that defines them. Migrate API destination plugins are managed by
* the \Drupal\migrate\Plugin\MigrateDestinationPluginManager class.
*
* @link https://api.drupal.org/api/drupal/namespace/Drupal!migrate!Plugin!migrate!destination List of destination plugins for Drupal configuration and content entities provided by the core Migrate module. @endlink
*

View File

@ -14,7 +14,13 @@ services:
arguments: [source, '@container.namespaces', '@cache.discovery', '@module_handler']
plugin.manager.migrate.process:
class: Drupal\migrate\Plugin\MigratePluginManager
arguments: [process, '@container.namespaces', '@cache.discovery', '@module_handler', 'Drupal\migrate\Annotation\MigrateProcessPlugin']
arguments:
- process
- '@container.namespaces'
- '@cache.discovery'
- '@module_handler'
- 'Drupal\migrate\Attribute\MigrateProcess'
- 'Drupal\migrate\Annotation\MigrateProcessPlugin'
plugin.manager.migrate.destination:
class: Drupal\migrate\Plugin\MigrateDestinationPluginManager
arguments: [destination, '@container.namespaces', '@cache.discovery', '@module_handler', '@entity_type.manager']

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\migrate\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
/**
* Defines a MigrateDestination attribute.
*
* Plugin Namespace: Plugin\migrate\destination
*
* For a working example, see
* \Drupal\migrate\Plugin\migrate\destination\UrlAlias
*
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Plugin\MigrateDestinationInterface
* @see \Drupal\migrate\Plugin\migrate\destination\DestinationBase
* @see \Drupal\migrate\Attribute\MigrateProcess
* @see \Drupal\migrate\Attribute\MigrateSource
* @see plugin_api
*
* @ingroup migration
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class MigrateDestination extends Plugin {
/**
* Constructs a migrate destination plugin attribute object.
*
* @param string $id
* A unique identifier for the destination plugin.
* @param bool $requirements_met
* (optional) Whether requirements are met.
* @param string|null $destination_module
* (optional) Identifies the system handling the data the destination plugin
* will write. The destination plugin itself determines how the value is
* used. For example, Migrate's destination plugins expect
* destination_module to be the name of a module that must be installed on
* the destination.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public bool $requirements_met = TRUE,
public readonly ?string $destination_module = NULL,
public readonly ?string $deriver = NULL,
) {
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\migrate\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
/**
* Defines a MigrateProcess attribute.
*
* Plugin Namespace: Plugin\migrate\process
*
* For a working example, see
* \Drupal\migrate\Plugin\migrate\process\DefaultValue
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
* @see \Drupal\migrate\ProcessPluginBase
* @see \Drupal\migrate\Attribute\MigrateDestination
* @see \Drupal\migrate\Attribute\MigrateSource
* @see plugin_api
*
* @ingroup migration
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class MigrateProcess extends Plugin {
/**
* Constructs a migrate process plugin attribute object.
*
* @param string $id
* A unique identifier for the process plugin.
* @param bool $handle_multiples
* (optional) Whether the plugin handles multiples itself. Typically these
* plugins will expect an array as input and iterate over it themselves,
* changing the whole array. For example the 'sub_process' and the 'flatten'
* plugins. If the plugin only needs to change a single value, then it can
* skip setting this attribute and let
* \Drupal\migrate\MigrateExecutable::processRow() handle the iteration.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly bool $handle_multiples = FALSE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\migrate\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
/**
* Defines a MigrateSource attribute.
*
* Plugin Namespace: Plugin\migrate\source
*
* For a working example, see
* \Drupal\migrate\Plugin\migrate\source\EmptySource
* \Drupal\migrate_drupal\Plugin\migrate\source\UrlAlias
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see \Drupal\migrate\Attribute\MigrateDestination
* @see \Drupal\migrate\Attribute\MigrateProcess
* @see plugin_api
*
* @ingroup migration
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class MigrateSource extends Plugin implements MultipleProviderAttributeInterface {
/**
* The providers of the source plugin.
*/
protected array $providers = [];
/**
* Constructs a migrate source plugin attribute object.
*
* @param string $id
* A unique identifier for the source plugin.
* @param string $source_module
* Identifies the system providing the data the source plugin will read.
* @param bool $requirements_met
* (optional) Whether requirements are met. Defaults to true. The source
* plugin itself determines how the value is used. For example, Migrate
* Drupal's source plugins expect source_module to be the name of a module
* that must be installed and enabled in the source database.
* @param mixed $minimum_version
* (optional) Specifies the minimum version of the source provider. This can
* be any type, and the source plugin itself determines how it is used. For
* example, Migrate Drupal's source plugins expect this to be an integer
* representing the minimum installed database schema version of the module
* specified by source_module.
* @param class-string|null $deriver
* (optional) The deriver class.
*
* @see \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase::checkRequirements
*/
public function __construct(
public readonly string $id,
public readonly string $source_module,
public bool $requirements_met = TRUE,
public readonly mixed $minimum_version = '',
public readonly ?string $deriver = NULL,
) {}
/**
* {@inheritdoc}
*/
public function setProvider(string $provider): void {
$this->setProviders([$provider]);
}
/**
* {@inheritdoc}
*/
public function getProviders(): array {
return $this->providers;
}
/**
* {@inheritdoc}
*/
public function setProviders(array $providers): void {
if ($providers) {
parent::setProvider(reset($providers));
}
else {
$this->provider = NULL;
}
$this->providers = $providers;
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\migrate\Attribute;
use Drupal\Component\Plugin\Attribute\AttributeInterface;
/**
* Defines a common interface for attributes with multiple providers.
*
* @internal
* 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.
*/
interface MultipleProviderAttributeInterface extends AttributeInterface {
/**
* Gets the name of the provider of the attribute class.
*
* @return string|null
* The provider of the attribute. If there are multiple providers the first
* is returned.
*/
public function getProvider(): ?string;
/**
* Gets the provider names of the attribute class.
*
* @return string[]
* The providers of the attribute.
*/
public function getProviders(): array;
/**
* Sets the provider names of the attribute class.
*
* @param string[] $providers
* The providers of the attribute.
*/
public function setProviders(array $providers): void;
}

View File

@ -3,12 +3,10 @@
namespace Drupal\migrate\Plugin\Discovery;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Drupal\Component\Annotation\AnnotationInterface;
use Drupal\Component\Annotation\Doctrine\StaticReflectionParser as BaseStaticReflectionParser;
use Drupal\Component\Annotation\Reflection\MockFileFinder;
use Drupal\Component\ClassFinder\ClassFinder;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\migrate\Annotation\MultipleProviderAnnotationInterface;
/**
* Determines providers based on a class's and its parent's namespaces.
@ -20,12 +18,7 @@ use Drupal\migrate\Annotation\MultipleProviderAnnotationInterface;
*/
class AnnotatedClassDiscoveryAutomatedProviders extends AnnotatedClassDiscovery {
/**
* A utility object that can use active autoloaders to find files for classes.
*
* @var \Drupal\Component\ClassFinder\ClassFinderInterface
*/
protected $finder;
use AnnotatedDiscoveryAutomatedProvidersTrait;
/**
* Constructs an AnnotatedClassDiscoveryAutomatedProviders object.
@ -48,26 +41,6 @@ class AnnotatedClassDiscoveryAutomatedProviders extends AnnotatedClassDiscovery
$this->finder = new ClassFinder();
}
/**
* {@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 namespaces) to the $providers array.
do {
$providers[] = $this->getProviderFromNamespace($parser->getNamespaceName());
} while ($parser = StaticReflectionParser::getParentParser($parser, $this->finder));
$providers = array_unique(array_filter($providers, function ($provider) {
return $provider && $provider !== 'component';
}));
$annotation->setProviders($providers);
}
/**
* {@inheritdoc}
*/

View File

@ -0,0 +1,59 @@
<?php
namespace Drupal\migrate\Plugin\Discovery;
use Drupal\Component\Annotation\AnnotationInterface;
use Drupal\Component\Annotation\Doctrine\StaticReflectionParser as BaseStaticReflectionParser;
use Drupal\migrate\Annotation\MultipleProviderAnnotationInterface;
/**
* Provides method for annotation discovery with multiple providers.
*/
trait AnnotatedDiscoveryAutomatedProvidersTrait {
/**
* A utility object that can use active autoloaders to find files for classes.
*
* @var \Drupal\Component\ClassFinder\ClassFinderInterface
*/
protected $finder;
/**
* Prepares the annotation definition.
*
* This is modified from the prepareAnnotationDefinition method from annotated
* class discovery to account for multiple providers.
*
* @param \Drupal\Component\Annotation\AnnotationInterface $annotation
* The annotation derived from the plugin.
* @param class-string $class
* The class used for the plugin.
* @param \Drupal\Component\Annotation\Doctrine\StaticReflectionParser|null $parser
* Static reflection parser.
*
* @see \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition()
* @see \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition()
*/
protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class, ?BaseStaticReflectionParser $parser = NULL): void {
if (!($annotation instanceof MultipleProviderAnnotationInterface)) {
throw new \LogicException('AnnotatedClassDiscoveryAutomatedProviders annotations must implement ' . MultipleProviderAnnotationInterface::class);
}
if (!$parser) {
throw new \LogicException('Parser argument must be passed for automated providers discovery.');
}
if (!method_exists($this, 'getProviderFromNamespace')) {
throw new \LogicException('Classes using \Drupal\migrate\Plugin\Discovery\AnnotatedDiscoveryAutomatedProvidersTrait must have getProviderFromNamespace() method.');
}
// @see \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition()
$annotation->setClass($class);
$providers = $annotation->getProviders();
// Loop through all the parent classes and add their providers (which we
// infer by parsing their namespaces) to the $providers array.
do {
$providers[] = $this->getProviderFromNamespace($parser->getNamespaceName());
} while ($parser = StaticReflectionParser::getParentParser($parser, $this->finder));
$providers = array_diff(array_unique(array_filter($providers)), ['component']);
$annotation->setProviders($providers);
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\migrate\Plugin\Discovery;
use Drupal\Component\Plugin\Attribute\AttributeInterface;
use Drupal\Core\Plugin\Discovery\AttributeClassDiscovery;
use Drupal\migrate\Attribute\MultipleProviderAttributeInterface;
/**
* Determines providers based on the namespaces of a class and its ancestors.
*
* @internal
* 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.
*/
class AttributeClassDiscoveryAutomatedProviders extends AttributeClassDiscovery {
/**
* {@inheritdoc}
*/
protected function prepareAttributeDefinition(AttributeInterface $attribute, string $class): void {
if (!($attribute instanceof MultipleProviderAttributeInterface)) {
throw new \LogicException('AttributeClassDiscoveryAutomatedProviders must implement ' . MultipleProviderAttributeInterface::class);
}
// @see Drupal\Component\Plugin\Discovery\AttributeClassDiscovery::prepareAttributeDefinition()
$attribute->setClass($class);
// Loop through all the parent classes and add their providers (which we
// infer by parsing their namespaces) to the $providers array.
$providers = $attribute->getProviders();
do {
$providers[] = $this->getProviderFromNamespace($class);
} while (($class = get_parent_class($class)) !== FALSE);
$providers = array_diff(array_unique(array_filter($providers)), ['component']);
$attribute->setProviders($providers);
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Drupal\migrate\Plugin\Discovery;
use Drupal\Component\Annotation\Doctrine\StaticReflectionParser as BaseStaticReflectionParser;
use Drupal\Component\Annotation\Reflection\MockFileFinder;
use Drupal\Component\ClassFinder\ClassFinder;
use Drupal\Component\Plugin\Attribute\AttributeInterface;
use Drupal\Core\Plugin\Discovery\AttributeDiscoveryWithAnnotations;
/**
* Enables both attribute and annotation discovery for plugin definitions.
*
* @internal
* 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.
*/
class AttributeDiscoveryWithAnnotationsAutomatedProviders extends AttributeDiscoveryWithAnnotations {
use AnnotatedDiscoveryAutomatedProvidersTrait;
/**
* Instance of attribute class discovery with automatic providers.
*
* Since there isn't multiple inheritance, instantiate the attribute only
* discovery for code reuse.
*
* @var \Drupal\migrate\Plugin\Discovery\AttributeClassDiscoveryAutomatedProviders
*/
private AttributeClassDiscoveryAutomatedProviders $attributeDiscovery;
public function __construct(
string $subdir,
\Traversable $rootNamespaces,
string $pluginDefinitionAttributeName = 'Drupal\Component\Plugin\Attribute\Plugin',
string $pluginDefinitionAnnotationName = 'Drupal\Component\Annotation\Plugin',
array $additionalNamespaces = [],
) {
parent::__construct($subdir, $rootNamespaces, $pluginDefinitionAttributeName, $pluginDefinitionAnnotationName, $additionalNamespaces);
$this->finder = new ClassFinder();
$this->attributeDiscovery = new AttributeClassDiscoveryAutomatedProviders($subdir, $rootNamespaces, $pluginDefinitionAttributeName);
}
/**
* {@inheritdoc}
*/
protected function prepareAttributeDefinition(AttributeInterface $attribute, string $class): void {
$this->attributeDiscovery->prepareAttributeDefinition($attribute, $class);
}
/**
* {@inheritdoc}
*/
protected function parseClass(string $class, \SplFileInfo $fileinfo): array {
// 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);
$reflection_class = $parser->getReflectionClass();
// @todo Handle deprecating definitions discovery via annotations in
// https://www.drupal.org/project/drupal/issues/3265945.
/** @var \Drupal\Component\Annotation\AnnotationInterface $annotation */
if ($annotation = $this->getAnnotationReader()->getClassAnnotation($reflection_class, $this->pluginDefinitionAnnotationName)) {
$this->prepareAnnotationDefinition($annotation, $class, $parser);
return ['id' => $annotation->getId(), 'content' => $annotation->get()];
}
// Annotations use static reflection and are able to analyze a class that
// extends classes or uses traits that do not exist. Attribute discovery
// will trigger a fatal error with such classes, so only call it if the
// class has a class attribute.
if ($reflection_class->hasClassAttribute($this->pluginDefinitionAttributeName)) {
return parent::parseClass($class, $fileinfo);
}
return ['id' => NULL, 'content' => NULL];
}
}

View File

@ -13,7 +13,7 @@ use Drupal\migrate\Row;
*
* @see \Drupal\migrate\Plugin\migrate\destination\DestinationBase
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see \Drupal\migrate\Attribute\MigrateDestination
* @see plugin_api
*
* @ingroup migration

View File

@ -5,13 +5,14 @@ namespace Drupal\migrate\Plugin;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\migrate\Attribute\MigrateDestination;
/**
* Plugin manager for migrate destination plugins.
*
* @see \Drupal\migrate\Plugin\MigrateDestinationInterface
* @see \Drupal\migrate\Plugin\migrate\destination\DestinationBase
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see \Drupal\migrate\Attribute\MigrateDestination
* @see plugin_api
*
* @ingroup migration
@ -40,12 +41,15 @@ class MigrateDestinationPluginManager extends MigratePluginManager {
* The module handler to invoke the alter hook with.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param string $attribute
* (optional) The attribute class name. Defaults to
* 'Drupal\migrate\Attribute\MigrateDestination'.
* @param string $annotation
* (optional) The annotation class name. Defaults to
* 'Drupal\migrate\Annotation\MigrateDestination'.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, $annotation = 'Drupal\migrate\Annotation\MigrateDestination') {
parent::__construct($type, $namespaces, $cache_backend, $module_handler, $annotation);
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, $attribute = MigrateDestination::class, $annotation = 'Drupal\migrate\Annotation\MigrateDestination') {
parent::__construct($type, $namespaces, $cache_backend, $module_handler, $attribute, $annotation);
$this->entityTypeManager = $entity_type_manager;
}

View File

@ -2,6 +2,8 @@
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\Attribute\AttributeInterface;
use Drupal\Component\Plugin\Attribute\PluginID;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
@ -11,10 +13,10 @@ use Drupal\Core\Plugin\DefaultPluginManager;
* Manages migrate plugins.
*
* @see hook_migrate_info_alter()
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Attribute\MigrateSource
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see \Drupal\migrate\Attribute\MigrateProcess
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
* @see \Drupal\migrate\Plugin\migrate\process\ProcessPluginBase
* @see plugin_api
@ -36,12 +38,20 @@ class MigratePluginManager extends DefaultPluginManager implements MigratePlugin
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
* @param string $attribute
* (optional) The attribute class name. Defaults to
* 'Drupal\Component\Plugin\Attribute\PluginID'.
* @param string $annotation
* (optional) The annotation class name. Defaults to
* 'Drupal\Component\Annotation\PluginID'.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, $annotation = 'Drupal\Component\Annotation\PluginID') {
parent::__construct("Plugin/migrate/$type", $namespaces, $module_handler, NULL, $annotation);
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, $attribute = PluginID::class, $annotation = 'Drupal\Component\Annotation\PluginID') {
if (!is_subclass_of($attribute, AttributeInterface::class)) {
// Backward compatibility.
$annotation = $attribute;
$attribute = PluginID::class;
}
parent::__construct("Plugin/migrate/$type", $namespaces, $module_handler, NULL, $attribute, $annotation);
$this->alterInfo('migrate_' . $type . '_info');
$this->setCacheBackend($cache_backend, 'migrate_plugins_' . $type);
}

View File

@ -15,7 +15,7 @@ use Drupal\migrate\Row;
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\ProcessPluginBase
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see \Drupal\migrate\Attribute\MigrateProcess
* @see plugin_api
*
* @ingroup migration

View File

@ -9,7 +9,7 @@ use Drupal\migrate\Row;
* Defines an interface for migrate sources.
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Attribute\MigrateSource
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see plugin_api
*

View File

@ -4,8 +4,10 @@ namespace Drupal\migrate\Plugin;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\migrate\Plugin\Discovery\AnnotatedClassDiscoveryAutomatedProviders;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\migrate\Plugin\Discovery\AnnotatedClassDiscoveryAutomatedProviders;
use Drupal\migrate\Plugin\Discovery\AttributeClassDiscoveryAutomatedProviders;
use Drupal\migrate\Plugin\Discovery\AttributeDiscoveryWithAnnotationsAutomatedProviders;
use Drupal\migrate\Plugin\Discovery\ProviderFilterDecorator;
/**
@ -13,7 +15,7 @@ use Drupal\migrate\Plugin\Discovery\ProviderFilterDecorator;
*
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Attribute\MigrateSource
* @see plugin_api
*
* @ingroup migration
@ -35,7 +37,7 @@ class MigrateSourcePluginManager extends MigratePluginManager {
* The module handler to invoke the alter hook with.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct($type, $namespaces, $cache_backend, $module_handler, 'Drupal\migrate\Annotation\MigrateSource');
parent::__construct($type, $namespaces, $cache_backend, $module_handler, 'Drupal\migrate\Attribute\MigrateSource', 'Drupal\migrate\Annotation\MigrateSource');
}
/**
@ -43,7 +45,30 @@ class MigrateSourcePluginManager extends MigratePluginManager {
*/
protected function getDiscovery() {
if (!$this->discovery) {
$discovery = new AnnotatedClassDiscoveryAutomatedProviders($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
if (isset($this->pluginDefinitionAttributeName) && isset($this->pluginDefinitionAnnotationName)) {
$discovery = new AttributeDiscoveryWithAnnotationsAutomatedProviders(
$this->subdir,
$this->namespaces,
$this->pluginDefinitionAttributeName,
$this->pluginDefinitionAnnotationName,
$this->additionalAnnotationNamespaces,
);
}
elseif (isset($this->pluginDefinitionAttributeName)) {
$discovery = new AttributeClassDiscoveryAutomatedProviders(
$this->subdir,
$this->namespaces,
$this->pluginDefinitionAttributeName,
);
}
else {
$discovery = new AnnotatedClassDiscoveryAutomatedProviders(
$this->subdir,
$this->namespaces,
$this->pluginDefinitionAnnotationName,
$this->additionalAnnotationNamespaces,
);
}
$this->discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
}
return $this->discovery;

View File

@ -21,7 +21,7 @@ use Drupal\migrate\Plugin\RequirementsInterface;
* information, refer to \Drupal\migrate\Plugin\MigrateDestinationInterface.
*
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see \Drupal\migrate\Attribute\MigrateDestination
* @see plugin_api
*
* @ingroup migration

View File

@ -2,6 +2,7 @@
namespace Drupal\migrate\Plugin\migrate\id_map;
use Drupal\Component\Plugin\Attribute\PluginID;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
@ -12,9 +13,8 @@ use Drupal\migrate\Row;
* Defines the null ID map implementation.
*
* This serves as a dummy in order to not store anything.
*
* @PluginID("null")
*/
#[PluginID('null')]
class NullIdMap extends PluginBase implements MigrateIdMapInterface {
/**

View File

@ -2,6 +2,7 @@
namespace Drupal\migrate\Plugin\migrate\id_map;
use Drupal\Component\Plugin\Attribute\PluginID;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\Exception\SchemaTableKeyTooLargeException;
@ -30,9 +31,8 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
*
* It creates one map and one message table per migration entity to store the
* relevant information.
*
* @PluginID("sql")
*/
#[PluginID('sql')]
class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface, HighestIdInterface {
/**

View File

@ -2,6 +2,7 @@
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
@ -86,11 +87,8 @@ use Drupal\migrate\Row;
* configuration, if foo is '', NULL or FALSE, then bar will be [].
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*
* @MigrateProcessPlugin(
* id = "explode"
* )
*/
#[MigrateProcess('explode')]
class Explode extends ProcessPluginBase {
/**

View File

@ -2,6 +2,7 @@
namespace Drupal\migrate\Plugin\migrate\source;
use Drupal\migrate\Attribute\MigrateSource;
use Drupal\migrate\Plugin\MigrationInterface;
/**
@ -40,12 +41,11 @@ use Drupal\migrate\Plugin\MigrationInterface;
*
* For additional configuration keys, refer to the parent class:
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "embedded_data",
* source_module = "migrate"
* )
*/
#[MigrateSource(
id: 'embedded_data',
source_module: 'migrate'
)]
class EmbeddedDataSource extends SourcePluginBase {
/**

View File

@ -2,6 +2,8 @@
namespace Drupal\migrate\Plugin\migrate\source;
use Drupal\migrate\Attribute\MigrateSource;
/**
* Source returning a row based on the constants provided.
*
@ -21,12 +23,11 @@ namespace Drupal\migrate\Plugin\migrate\source;
*
* For additional configuration keys, refer to the parent class:
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "empty",
* source_module = "migrate"
* )
*/
#[MigrateSource(
id: 'empty',
source_module: 'migrate',
)]
class EmptySource extends SourcePluginBase {
/**

View File

@ -105,7 +105,7 @@ use Drupal\migrate\Row;
* In this example, the constant 'foo' is defined with a value of 'bar'. It is
* later used in the process pipeline to set the value of the field baz.
*
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Attribute\MigrateSource
* @see \Drupal\migrate\Plugin\MigrateIdMapInterface
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Plugin\MigrateSourceInterface

View File

@ -21,7 +21,7 @@ use Drupal\migrate\Plugin\MigrateProcessInterface;
* @see https://www.drupal.org/node/2129651
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see \Drupal\migrate\Attribute\MigrateProcess
* @see \Drupal\migrate\Plugin\migrate\process\SkipOnEmpty
* @see d7_field_formatter_settings.yml
* @see plugin_api

View File

@ -0,0 +1,5 @@
name: 'Migrate module source annotation bc tests'
type: module
description: 'Support module for source plugin annotation discovery backwards compatibility tests'
package: Testing
version: VERSION

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\migrate_source_annotation_bc_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
/**
* A migration source plugin with annotations and a single provider.
*
* This plugin exists to test backwards compatibility of source plugin discovery
* for plugin classes using annotations. This class has no providers other than
* 'migrate_source_annotation_bc_test' and 'core'. This class and its annotation
* should remain until annotation support is completely removed.
*
* @MigrateSource(
* id = "annotated",
* source_module = "migrate"
* )
*/
class MigrateSourceWithAnnotations extends SourcePluginBase {
/**
* {@inheritdoc}
*/
public function fields() {
return [];
}
/**
* {@inheritdoc}
*/
public function __toString() {
return 'Annotated';
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [];
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
return new \ArrayIterator();
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Drupal\migrate_source_annotation_bc_test\Plugin\migrate\source;
use Drupal\migrate_drupal\Plugin\migrate\source\EmptySource;
/**
* A migration source plugin with annotations and multiple providers.
*
* This plugin exists to test backwards compatibility of source plugin discovery
* for plugin classes using annotations. This class has an additional provider,
* because it extends a plugin in migrate_drupal. This class and its annotation
* should remain until annotation support is completely removed.
*
* @MigrateSource(
* id = "annotated_multiple_providers",
* source_module = "migrate"
* )
*/
class MigrateSourceWithAnnotationsMultipleProviders extends EmptySource {
/**
* {@inheritdoc}
*/
public function __toString() {
return 'Annotated multiple providers';
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests discovery of source plugins with annotations.
*
* Migrate source plugins use a specific discovery class to accommodate multiple
* providers. This is a backwards compatibility test that discovery for plugin
* classes that have annotations still works even after all core plugins have
* been converted to attributes.
*
* @group migrate
*/
class MigrateSourceAnnotationDiscoveryTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate'];
/**
* @covers \Drupal\migrate\Plugin\MigrateSourcePluginManager::getDefinitions
*/
public function testGetDefinitions(): void {
// First, test attribute-only discovery.
$expected = ['embedded_data', 'empty'];
$source_plugins = $this->container->get('plugin.manager.migrate.source')->getDefinitions();
ksort($source_plugins);
$this->assertSame($expected, array_keys($source_plugins));
// Next, test discovery of both attributed and annotated plugins. The
// annotated plugin with multiple providers depends on migrate_drupal and
// should not be discovered with it uninstalled.
$expected = ['annotated', 'embedded_data', 'empty'];
$this->enableModules(['migrate_source_annotation_bc_test']);
$source_plugins = $this->container->get('plugin.manager.migrate.source')->getDefinitions();
ksort($source_plugins);
$this->assertSame($expected, array_keys($source_plugins));
// Install migrate_drupal and now the annotated plugin that depends on it
// should be discovered.
$expected = [
'annotated',
'annotated_multiple_providers',
'embedded_data',
'empty',
];
$this->enableModules(['migrate_drupal']);
$source_plugins = $this->container->get('plugin.manager.migrate.source')->getDefinitions();
// Confirming here the that the source plugins that migrate and
// migrate_source_annotation_bc_test are discovered. There are additional
// plugins provided by migrate_drupal, but they do not need to be enumerated
// here.
$this->assertSame(array_diff($expected, array_keys($source_plugins)), []);
}
}

View File

@ -6,6 +6,7 @@ services:
- '@container.namespaces'
- '@cache.discovery'
- '@module_handler'
- '\Drupal\migrate_drupal\Attribute\MigrateField'
- '\Drupal\migrate_drupal\Annotation\MigrateField'
Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface: '@plugin.manager.migrate.field'
logger.channel.migrate_drupal:

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\migrate_drupal\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
/**
* Defines a field plugin attribute object.
*
* Field plugins are responsible for handling the migration of custom fields
* (provided by Field API in Drupal 7) to Drupal 8+. They are allowed to alter
* fieldable entity migrations when these migrations are being generated, and
* can compute destination field types for individual fields during the actual
* migration process.
*
* Plugin Namespace: Plugin\migrate\field
*
* For a working example, see
* \Drupal\datetime\Plugin\migrate\field\DateField
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate_drupal\Plugin\MigrateFieldInterface;
* @see \Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase
* @see plugin_api
*
* @ingroup migration
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class MigrateField extends Plugin {
/**
* The plugin definition.
*
* @var array
*/
protected $definition;
/**
* Constructs a migrate field attribute object.
*
* @param string $id
* A unique identifier for the field plugin.
* @param int[] $core
* (optional) The Drupal core version(s) this plugin applies to.
* @param int $weight
* (optional) The weight of this plugin relative to other plugins servicing
* the same field type and core version. The lowest weighted applicable
* plugin will be used for each field.
* @param string[] $type_map
* (optional) Map of D6 and D7 field types to D8+ field type plugin IDs.
* @param string|null $source_module
* (optional) Identifies the system providing the data the field plugin will
* read. The source_module is expected to be the name of a Drupal module
* that must be installed in the source database.
* @param string|null $destination_module
* (optional) Identifies the system handling the data the destination plugin
* will write. The destination_module is expected to be the name of a Drupal
* module on the destination site that must be installed.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly array $core = [6],
public readonly int $weight = 0,
public readonly array $type_map = [],
public readonly ?string $source_module = NULL,
public readonly ?string $destination_module = NULL,
public readonly ?string $deriver = NULL
) {}
}

View File

@ -11,7 +11,7 @@ use Drupal\migrate\Plugin\MigrationInterface;
* Plugin manager for migrate field plugins.
*
* @see \Drupal\migrate_drupal\Plugin\MigrateFieldInterface
* @see \Drupal\migrate\Annotation\MigrateField
* @see \Drupal\migrate\Attribute\MigrateField
* @see plugin_api
*
* @ingroup migration
@ -48,7 +48,7 @@ class MigrateFieldPluginManager extends MigratePluginManager implements MigrateF
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* If the plugin cannot be determined, such as if the field type is invalid.
*
* @see \Drupal\migrate_drupal\Annotation\MigrateField
* @see \Drupal\migrate_drupal\Attribute\MigrateField
*/
public function getPluginIdFromFieldType($field_type, array $configuration = [], MigrationInterface $migration = NULL) {
$core = static::DEFAULT_CORE_VERSION;

View File

@ -11,7 +11,7 @@ use Drupal\migrate_drupal\Plugin\MigrateFieldInterface;
* The base class for all field plugins.
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate_drupal\Annotation\MigrateField
* @see \Drupal\migrate_drupal\Attribute\MigrateField
* @see \Drupal\migrate_drupal\Plugin\MigrateFieldInterface
* @see plugin_api
*

View File

@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateSource;
use Drupal\migrate\EntityFieldDefinitionTrait;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate\Plugin\MigrateSourceInterface;
@ -61,13 +62,12 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* For additional configuration keys, refer to the parent class:
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "content_entity",
* source_module = "migrate_drupal",
* deriver = "\Drupal\migrate_drupal\Plugin\migrate\source\ContentEntityDeriver",
* )
*/
*/
#[MigrateSource(
id: "content_entity",
source_module: "migrate_drupal",
deriver: ContentEntityDeriver::class,
)]
class ContentEntity extends SourcePluginBase implements ContainerFactoryPluginInterface {
use EntityFieldDefinitionTrait;