first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
---
label: 'Migrating, updating, and upgrading'
top_level: true
---
<h2>{% trans %}What are updating, upgrading, and migrating?{% endtrans %}</h2>
<p>{% trans %}<em>Updating</em> is the process of changing from one minor version of the software to a newer version, such as from version 8.3.4 to 8.3.5, or 8.3.5 to 8.4.0. Starting with version 8.x, you can also update to major versions 9, 10, and beyond if your add-on modules, themes, and install profiles are compatible. <em>Upgrading</em> is the process of changing from an older major version of the software to a newer version, such as from version 7 to 8. <em>Migrating</em> is the process of importing data into a site.{% endtrans %}</p>
<p>{% trans %}To upgrade a site from Drupal 6 or 7 to Drupal 8 or later, keeping the content and configuration the same, you will install the new version of the software and add-on modules and themes in a new site, and then migrate the content and other data from your old site into the new site.{% endtrans %}</p>
<h2>{% trans %}Overview of Migrating{% endtrans %}</h2>
<p>{% trans %}You can use the <em>Migration</em> group of modules to perform the migration step of upgrading from Drupal 6 or 7 to Drupal 8 or later, as well as other migrations. These modules also provide APIs that can be used by programmers to write custom software for migrations. Here are the functions of the core migration modules:{% endtrans %}</p>
<dl>
<dt>{% trans %}Migrate{% endtrans %}</dt>
<dd>{% trans %}Provides the underlying API for migrating data.{% endtrans %}</dd>
<dt>{% trans %}Migrate Drupal{% endtrans %}</dt>
<dd>{% trans %}Provides data migration from older versions of the core software into a new site.{% endtrans %}</dd>
<dt>{% trans %}Migrate Drupal UI{% endtrans %}</dt>
<dd>{% trans %}Provides a user interface for performing data migration from older versions of the core software into a new site.{% endtrans %}</dd>
</dl>
<p>{% trans %}If the source of the data you want to migrate is a different content management system, or if the data source is a site that was built using contributed modules that the core migration modules do not support, then you will also need one or more contributed or custom modules in order to migrate your data.{% endtrans %}</p>
<h2>{% trans %}Additional Resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/upgrading-drupal">Upgrading Drupal</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,211 @@
<?php
/**
* @file
* Hooks provided by the Migrate module.
*/
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
/**
* @defgroup migration Migrate API
* @{
* Overview of the Migrate API, which migrates data into Drupal.
*
* @section overview Overview of a migration
* Migration is an
* @link http://wikipedia.org/wiki/Extract,_transform,_load Extract, Transform, Load @endlink
* (ETL) process. In the Drupal Migrate API, the extract phase is called
* 'source', the transform phase is called 'process', and the load phase is
* called 'destination'. It is important to understand that the term 'load' in
* ETL refers to loading data into the storage while in a typical Drupal context
* the term 'load' refers to loading data from storage.
*
* In the source phase, a set of data, called the row, is retrieved from the
* data source. The data can be migrated from a database, loaded from a file
* (for example CSV, JSON or XML) or fetched from a web service (for example RSS
* or REST). The row is sent to the process phase where it is transformed as
* needed or marked to be skipped. After processing, the transformed row is
* passed to the destination phase where it is loaded (saved) into the target
* Drupal site.
*
* Migrate API uses the Drupal plugin system for many different purposes. Most
* importantly, the overall ETL process is defined as a migration plugin and the
* three phases (source, process and destination) have their own plugin types.
*
* @section sec_migrations Migrate API migration plugins
* Migration plugin definitions are stored in a module's 'migrations' directory.
* The plugin class is \Drupal\migrate\Plugin\Migration, with interface
* \Drupal\migrate\Plugin\MigrationInterface. Migration plugins are managed by
* the \Drupal\migrate\Plugin\MigrationPluginManager class. Migration plugins
* are only available if the providers of their source plugins are installed.
*
* @link https://www.drupal.org/docs/8/api/migrate-api/migrate-destination-plugins-examples Example migrations in Migrate API handbook. @endlink
*
* @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
* 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.
*
* @link https://api.drupal.org/api/drupal/namespace/Drupal!migrate!Plugin!migrate!source List of source plugins provided by the core Migrate module. @endlink
* @link https://www.drupal.org/docs/8/api/migrate-api/migrate-source-plugins Core and contributed source plugin usage examples in Migrate API handbook. @endlink
*
* @section sec_process Migrate API process plugins
* Migrate API process plugins implement
* \Drupal\migrate\Plugin\MigrateProcessInterface and usually extend
* \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.
*
* @link https://api.drupal.org/api/drupal/namespace/Drupal!migrate!Plugin!migrate!process List of process plugins for common operations provided by the core Migrate module. @endlink
*
* @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 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
*
* @section sec_key_concepts Migrate API key concepts
* @subsection sec_stubs Stubs
* Taxonomy terms are an example of a data structure where an entity can have a
* reference to a parent. When a term is being migrated, it is possible that its
* parent term has not yet been migrated. Migrate API addresses this 'chicken
* and egg' dilemma by creating a stub term for the parent so that the child
* term can establish a reference to it. When the parent term is eventually
* migrated, Migrate API updates the previously created stub with the actual
* content.
*
* @subsection sec_map_tables Map tables
* Once a migrated row is saved and the destination IDs are known, Migrate API
* saves the source IDs, destination IDs, and the row hash into a map table. The
* source IDs and the hash facilitate tracking changes for continuous
* migrations. Other migrations can use the map tables for lookup purposes when
* establishing relationships between records.
*
* @subsection sec_high_water_mark High-water mark
* A High-water mark allows the Migrate API to track changes so that only data
* that has been created or updated in the source since the migration was
* previously executed is migrated. The only requirement to use the high-water
* feature is to declare the row property to use for the high-water mark. This
* can be any property that indicates the highest value migrated so far. For
* example, a timestamp property that indicates when a row of data was created
* or last updated would make an excellent high-water property. If the migration
* is executed again, only those rows that have a higher timestamp than in the
* previous migration would be included.
*
* @code
* source:
* plugin: d7_node
* high_water_property:
* name: changed
* @endcode
*
* In this example, the row property 'changed' is the high_water_property. If
* the value of 'changed' is greater than the current high-water mark the row
* is processed and the value of the high-water mark is updated to the value of
* 'changed'.
*
* @subsection sec_rollbacks Rollbacks
* When developing a migration, it is quite typical that the first version does
* not provide correct results for all migrated data. Rollbacks allow you to
* undo a migration and then execute it again after adjusting it.
*
* @section sec_more_info Documentation handbooks
* @link https://www.drupal.org/docs/8/api/migrate-api Migrate API handbook. @endlink
* @link https://www.drupal.org/docs/8/upgrade Upgrading to Drupal 8 handbook. @endlink
* @}
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Allows adding data to a row before processing it.
*
* For example, filter module used to store filter format settings in the
* variables table which now needs to be inside the filter format config
* file. So, it needs to be added here.
*
* hook_migrate_MIGRATION_ID_prepare_row() is also available.
*
* @param \Drupal\migrate\Row $row
* The row being imported.
* @param \Drupal\migrate\Plugin\MigrateSourceInterface $source
* The source migration.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The current migration.
*
* @ingroup migration
*/
function hook_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
if ($migration->id() == 'd6_filter_formats') {
$value = $source->getDatabase()->query('SELECT [value] FROM {variable} WHERE [name] = :name', [':name' => 'my_module_filter_foo_' . $row->getSourceProperty('format')])->fetchField();
if ($value) {
$row->setSourceProperty('settings:my_module:foo', unserialize($value));
}
}
}
/**
* Allows adding data to a row for a migration with the specified ID.
*
* This provides the same functionality as hook_migrate_prepare_row() but
* removes the need to check the value of $migration->id().
*
* @param \Drupal\migrate\Row $row
* The row being imported.
* @param \Drupal\migrate\Plugin\MigrateSourceInterface $source
* The source migration.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The current migration.
*
* @ingroup migration
*/
function hook_migrate_MIGRATION_ID_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
$value = $source->getDatabase()->query('SELECT [value] FROM {variable} WHERE [name] = :name', [':name' => 'my_module_filter_foo_' . $row->getSourceProperty('format')])->fetchField();
if ($value) {
$row->setSourceProperty('settings:my_module:foo', unserialize($value));
}
}
/**
* Allows altering the list of discovered migration plugins.
*
* Modules are able to alter specific migrations structures or even remove or
* append additional migrations to the discovery. For example, this
* implementation filters out Drupal 6 migrations from the discovered migration
* list. This is done by checking the migration tags.
*
* @param array[] $migrations
* An associative array of migrations keyed by migration ID. Each value is the
* migration array, obtained by decoding the migration YAML file and enriched
* with some meta information added during discovery phase, like migration
* 'class', 'provider' or '_discovered_file_path'.
*
* @ingroup migration
*/
function hook_migration_plugins_alter(array &$migrations) {
$migrations = array_filter($migrations, function (array $migration) {
$tags = isset($migration['migration_tags']) ? (array) $migration['migration_tags'] : [];
return !in_array('Drupal 6', $tags);
});
}
/**
* @} End of "addtogroup hooks".
*/

View File

@@ -0,0 +1,10 @@
name: Migrate
type: module
description: 'Provides a framework for migrating data to Drupal.'
package: Migration
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains install and update functions for Migrate.
*/
/**
* Implements hook_update_last_removed().
*/
function migrate_update_last_removed() {
return 8001;
}
/**
* Remove the year 2038 date limitation.
*/
function migrate_update_10100(&$sandbox = NULL) {
$connection = \Drupal::database();
$tables = $connection->schema()->findTables('migrate_map_%');
if (!empty($tables) && $connection->databaseType() != 'sqlite') {
foreach ($tables as $table) {
$new = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'UNIX timestamp of the last time this row was imported',
'size' => 'big',
];
$connection->schema()->changeField($table, 'last_imported', 'last_imported', $new);
}
}
}

View File

@@ -0,0 +1,6 @@
migrate.messages:
title: Migration messages
parent: system.admin_reports
description: View the migration messages.
route_name: migrate.messages
weight: 0

View File

@@ -0,0 +1,22 @@
<?php
/**
* @file
* Provides the Migrate API.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function migrate_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.migrate':
$output = '<h2>' . t('About') . '</h2>';
$output .= '<p>';
$output .= t('The Migrate module provides a framework for migrating data, usually from an external source into your site. It does not provide a user interface. For more information, see the <a href=":migrate">online documentation for the Migrate module</a>.', [':migrate' => 'https://www.drupal.org/documentation/modules/migrate']);
$output .= '</p>';
return $output;
}
}

View File

@@ -0,0 +1,2 @@
view migration messages:
title: 'View migration messages'

View File

@@ -0,0 +1,15 @@
<?php
/**
* @file
* Post update functions for migrate.
*/
/**
* Implements hook_removed_post_updates().
*/
function migrate_removed_post_updates() {
return [
'migrate_post_update_clear_migrate_source_count_cache' => '10.0.0',
];
}

View File

@@ -0,0 +1,15 @@
migrate.messages:
path: '/admin/reports/migration-messages'
defaults:
_controller: '\Drupal\migrate\Controller\MigrateMessageController::overview'
_title: 'Migration messages'
requirements:
_permission: 'view migration messages'
migrate.messages.detail:
path: '/admin/reports/migration-messages/{migration_id}'
defaults:
_controller: '\Drupal\migrate\Controller\MigrateMessageController::details'
_title_callback: '\Drupal\migrate\Controller\MigrateMessageController::title'
requirements:
_permission: 'view migration messages'

View File

@@ -0,0 +1,48 @@
services:
_defaults:
autoconfigure: true
migrate.plugin_event_subscriber:
class: Drupal\migrate\Plugin\PluginEventSubscriber
cache.migrate:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory: ['@cache_factory', 'get']
arguments: [migrate]
plugin.manager.migrate.source:
class: Drupal\migrate\Plugin\MigrateSourcePluginManager
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\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']
plugin.manager.migrate.id_map:
class: Drupal\migrate\Plugin\MigratePluginManager
arguments: [id_map, '@container.namespaces', '@cache.discovery', '@module_handler']
cache.discovery_migration:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory: ['@cache_factory', 'get']
arguments: [discovery_migration]
plugin.manager.migration:
class: Drupal\migrate\Plugin\MigrationPluginManager
arguments: ['@module_handler', '@cache.discovery_migration', '@language_manager']
Drupal\migrate\Plugin\MigrationPluginManagerInterface: '@plugin.manager.migration'
Drupal\migrate\MigrateBuildDependencyInterface: '@plugin.manager.migration'
migrate.lookup:
class: Drupal\migrate\MigrateLookup
arguments: ['@plugin.manager.migration']
Drupal\migrate\MigrateLookupInterface: '@migrate.lookup'
migrate.stub:
class: Drupal\migrate\MigrateStub
arguments: ['@plugin.manager.migration']
Drupal\migrate\MigrateStubInterface: '@migrate.stub'

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\migrate\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a migration destination plugin annotation object.
*
* Plugin Namespace: Plugin\migrate\destination
*
* For a working example, see
* \Drupal\migrate\Plugin\migrate\destination\UrlAlias
*
* @see \Drupal\migrate\Plugin\MigrateDestinationInterface
* @see \Drupal\migrate\Plugin\migrate\destination\DestinationBase
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see plugin_api
*
* @ingroup migration
*
* @Annotation
*/
class MigrateDestination extends Plugin {
/**
* A unique identifier for the process plugin.
*
* @var string
*/
public $id;
/**
* Whether requirements are met.
*
* If TRUE and a 'provider' key is present in the annotation then the
* default destination plugin manager will set this to FALSE if the
* provider (module/theme) doesn't exist.
*
* @var bool
*/
public $requirements_met = TRUE;
/**
* Identifies the system handling the data the destination plugin will write.
*
* The destination plugin itself determines how the value is used. For
* example, Migrate Drupal's destination plugins expect destination_module to
* be the name of a module that must be installed on the destination.
*
* @var string
*/
public $destination_module;
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\migrate\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a migration process plugin annotation object.
*
* 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\Annotation\MigrateSource
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*
* @Annotation
*/
class MigrateProcessPlugin extends Plugin {
/**
* A unique identifier for the process plugin.
*
* @var string
*/
public $id;
/**
* Whether the plugin handles multiples itself.
*
* This property is optional and it does not need to be declared.
*
* 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 need to change a single value it
* can skip setting this attribute and let
* \Drupal\migrate\MigrateExecutable::processRow() handle the iteration.
*
* @var bool
*/
public $handle_multiples = FALSE;
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Drupal\migrate\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a migration source plugin annotation object.
*
* Plugin Namespace: Plugin\migrate\source
*
* For a working example, check
* \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\Annotation\MigrateProcessPlugin
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*
* @Annotation
*/
class MigrateSource extends Plugin implements MultipleProviderAnnotationInterface {
/**
* A unique identifier for the process plugin.
*
* @var string
*/
public $id;
/**
* Whether requirements are met.
*
* @var bool
*/
public $requirements_met = TRUE;
/**
* Identifies the system providing the data the source plugin will read.
*
* 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.
*
* @see \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase::checkRequirements
*
* @var string
*/
public $source_module;
/**
* 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.
*
* @var mixed
*/
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;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\migrate\Annotation;
use Drupal\Component\Annotation\AnnotationInterface;
/**
* Defines a common interface for classed annotations with multiple providers.
*
* @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.
*/
interface MultipleProviderAnnotationInterface extends AnnotationInterface {
/**
* Gets the name of the provider of the annotated class.
*
* @return string
* The provider of the annotation. If there are multiple providers the first
* is returned.
*/
public function getProvider();
/**
* Gets the provider names of the annotated class.
*
* @return string[]
* The providers of the annotation.
*/
public function getProviders();
/**
* Sets the provider names of the annotated class.
*
* @param string[] $providers
* The providers of the annotation.
*/
public function setProviders(array $providers);
}

View File

@@ -0,0 +1,52 @@
<?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 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,49 @@
<?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 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,27 @@
<?php
namespace Drupal\migrate\Audit;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Defines an exception to throw if an error occurs during a migration audit.
*/
class AuditException extends \RuntimeException {
/**
* AuditException constructor.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration that caused the exception.
* @param string $message
* The reason the audit failed.
* @param \Exception $previous
* (optional) The previous exception.
*/
public function __construct(MigrationInterface $migration, $message, ?\Exception $previous = NULL) {
$message = sprintf('Cannot audit migration %s: %s', $migration->id(), $message);
parent::__construct($message, 0, $previous);
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Drupal\migrate\Audit;
use Drupal\Component\Render\MarkupInterface;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Encapsulates the result of a migration audit.
*/
class AuditResult implements MarkupInterface, \Countable {
/**
* The audited migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The result of the audit (TRUE if passed, FALSE otherwise).
*
* @var bool
*/
protected $status;
/**
* The reasons why the migration passed or failed the audit.
*
* @var string[]
*/
protected $reasons = [];
/**
* AuditResult constructor.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The audited migration.
* @param bool $status
* The result of the audit (TRUE if passed, FALSE otherwise).
* @param string[] $reasons
* (optional) The reasons why the migration passed or failed the audit.
*/
public function __construct(MigrationInterface $migration, $status, array $reasons = []) {
if (!is_bool($status)) {
throw new \InvalidArgumentException('Audit results must have a boolean status.');
}
$this->migration = $migration;
$this->status = $status;
array_walk($reasons, [$this, 'addReason']);
}
/**
* Returns the audited migration.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The audited migration.
*/
public function getMigration() {
return $this->migration;
}
/**
* Returns the boolean result of the audit.
*
* @return bool
* The result of the audit. TRUE if the migration passed the audit, FALSE
* otherwise.
*/
public function passed() {
return $this->status;
}
/**
* Adds a reason why the migration passed or failed the audit.
*
* @param string|object $reason
* The reason to add. Can be a string or a string-castable object.
*
* @return $this
*/
public function addReason($reason) {
array_push($this->reasons, (string) $reason);
return $this;
}
/**
* Creates a passing audit result for a migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The audited migration.
* @param string[] $reasons
* (optional) The reasons why the migration passed the audit.
*
* @return static
*/
public static function pass(MigrationInterface $migration, array $reasons = []) {
return new static($migration, TRUE, $reasons);
}
/**
* Creates a failing audit result for a migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The audited migration.
* @param array $reasons
* (optional) The reasons why the migration failed the audit.
*
* @return static
*/
public static function fail(MigrationInterface $migration, array $reasons = []) {
return new static($migration, FALSE, $reasons);
}
/**
* Implements \Countable::count() for Twig template compatibility.
*
* @return int
*
* @see \Drupal\Component\Render\MarkupInterface
*/
#[\ReturnTypeWillChange]
public function count() {
return count($this->reasons);
}
/**
* Returns the reasons the migration passed or failed, as a string.
*
* @return string
*
* @see \Drupal\Component\Render\MarkupInterface
*/
public function __toString() {
return implode("\n", $this->reasons);
}
/**
* Returns the reasons the migration passed or failed, for JSON serialization.
*
* @return string[]
*/
#[\ReturnTypeWillChange]
public function jsonSerialize() {
return $this->reasons;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\migrate\Audit;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Defines an interface for migration auditors.
*
* A migration auditor is a class which can examine a migration to determine if
* it will cause conflicts with data already existing in the destination system.
* What kind of auditing it does, and how it does it, is up to the implementing
* class.
*/
interface AuditorInterface {
/**
* Audits a migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to audit.
*
* @throws \Drupal\migrate\Audit\AuditException
* If the audit fails.
*
* @return \Drupal\migrate\Audit\AuditResult
* The result of the audit.
*/
public function audit(MigrationInterface $migration);
/**
* Audits a set of migrations.
*
* @param \Drupal\migrate\Plugin\MigrationInterface[] $migrations
* The migrations to audit.
*
* @return \Drupal\migrate\Audit\AuditResult[]
* The audit results, keyed by migration ID.
*/
public function auditMultiple(array $migrations);
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate\Audit;
/**
* Defines an interface for destination and ID maps which track a highest ID.
*
* When implemented by destination plugins, getHighestId() should return the
* highest ID of the destination entity type that exists in the system. So, for
* example, the entity:node plugin should return the highest node ID that
* exists, regardless of whether it was created by a migration.
*
* When implemented by an ID map, getHighestId() should return the highest
* migrated ID of the destination entity type.
*/
interface HighestIdInterface {
/**
* Returns the highest ID tracked by the implementing plugin.
*
* @return int
* The highest ID.
*/
public function getHighestId();
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Drupal\migrate\Audit;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\migrate\Plugin\migrate\destination\EntityContentComplete;
use Drupal\migrate\Plugin\MigrationInterface;
// cspell:ignore destid
/**
* Audits migrations that create content entities in the destination system.
*/
class IdAuditor implements AuditorInterface {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function audit(MigrationInterface $migration) {
// If the migration does not opt into auditing, it passes.
if (!$migration->isAuditable()) {
return AuditResult::pass($migration);
}
$interface = HighestIdInterface::class;
$destination = $migration->getDestinationPlugin();
if (!$destination instanceof HighestIdInterface) {
throw new AuditException($migration, "Destination does not implement $interface");
}
$id_map = $migration->getIdMap();
if (!$id_map instanceof HighestIdInterface) {
throw new AuditException($migration, "ID map does not implement $interface");
}
if ($destination->getHighestId() > $id_map->getHighestId() || ($destination instanceof EntityContentComplete && !$this->auditEntityComplete($migration))) {
return AuditResult::fail($migration, [
$this->t('The destination system contains data which was not created by a migration.'),
]);
}
return AuditResult::pass($migration);
}
/**
* {@inheritdoc}
*/
public function auditMultiple(array $migrations) {
$conflicts = [];
foreach ($migrations as $migration) {
$migration_id = $migration->getPluginId();
$conflicts[$migration_id] = $this->audit($migration);
}
ksort($conflicts);
return $conflicts;
}
/**
* Audits an EntityComplete migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to audit.
*
* @return bool
* TRUE if the audit passes and FALSE if not.
*
* @todo Refactor in https://www.drupal.org/project/drupal/issues/3061676 or
* https://www.drupal.org/project/drupal/issues/3091004
*/
private function auditEntityComplete(MigrationInterface $migration) {
$map_table = $migration->getIdMap()->mapTableName();
$database = \Drupal::database();
if (!$database->schema()->tableExists($map_table)) {
throw new \InvalidArgumentException();
}
$query = $database->select($map_table, 'map')
->fields('map', ['destid2'])
->range(0, 1)
->orderBy('destid2', 'DESC');
$max = (int) $query->execute()->fetchField();
// Make a migration based on node_complete but with an entity_revision
// destination.
$revision_migration = $migration->getPluginDefinition();
$revision_migration['id'] = $migration->getPluginId() . '-revision';
$revision_migration['destination']['plugin'] = 'entity_revision:node';
$revision_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($revision_migration);
// Get the highest node revision ID.
$destination = $revision_migration->getDestinationPlugin();
$highest = $destination->getHighestId();
return $max <= $highest;
}
}

View File

@@ -0,0 +1,307 @@
<?php
namespace Drupal\migrate\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseConnectionRefusedException;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
// cspell:ignore sourceid
/**
* Provides controller methods for the Message form.
*/
class MigrateMessageController extends ControllerBase {
/**
* Constructs a MigrateController.
*
* @param \Drupal\Core\Database\Connection $database
* A database connection.
* @param \Drupal\Core\Form\FormBuilderInterface $formBuilder
* The form builder service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migrationPluginManager
* The migration plugin manager.
*/
public function __construct(
protected Connection $database,
FormBuilderInterface $formBuilder,
protected MigrationPluginManagerInterface $migrationPluginManager,
) {
$this->formBuilder = $formBuilder;
}
/**
* Displays an overview of migrate messages.
*
* @return array
* A render array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
public function overview(): array {
// Check if there are migrate_message tables.
$tables = $this->database->schema()->findTables('migrate_message_%');
if (empty($tables)) {
$build['no_tables'] = [
'#type' => 'item',
'#markup' => $this->t('There are no migration message tables.'),
];
return $build;
}
// There are migrate_message tables so build the overview form.
$migrations = $this->migrationPluginManager->createInstances([]);
$header = [
$this->t('Migration'),
$this->t('Machine Name'),
$this->t('Messages'),
];
// Display the number of messages for each migration.
$rows = [];
foreach ($migrations as $id => $migration) {
$message_count = $migration->getIdMap()->messageCount();
// The message count is zero when there are no messages or when the
// message table does not exist.
if ($message_count == 0) {
continue;
}
$row = [];
$row['label'] = $migration->label();
$row['machine_name'] = $id;
$route_parameters = [
'migration_id' => $migration->id(),
];
$row['messages'] = [
'data' => [
'#type' => 'link',
'#title' => $message_count,
'#url' => Url::fromRoute('migrate.messages.detail', $route_parameters),
],
];
$rows[] = $row;
}
$build['migrations_table'] = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No migration messages available.'),
];
$build['message_pager'] = ['#type' => 'pager'];
return $build;
}
/**
* Displays a listing of migration messages for the given migration ID.
*
* @param string $migration_id
* A migration ID.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return array
* A render array.
*/
public function details(string $migration_id, Request $request): array {
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->migrationPluginManager->createInstance($migration_id);
if (!$migration) {
throw new NotFoundHttpException();
}
// Get the map and message table names.
$map_table = $migration->getIdMap()->mapTableName();
$message_table = $migration->getIdMap()->messageTableName();
// If the map table does not exist then do not continue.
if (!$this->database->schema()->tableExists($map_table)) {
throw new NotFoundHttpException();
}
// If there is a map table but no message table display an error.
if (!$this->database->schema()->tableExists($message_table)) {
$this->messenger()->addError($this->t('The message table is missing for this migration.'));
return [];
}
// Create the column header names.
$header = [];
$source_plugin = $migration->getSourcePlugin();
// Create the column header names from the source plugin fields() method.
// Fallback to the source_id name when the source ID is missing from
// fields() method.
try {
$fields = $source_plugin->fields();
}
catch (DatabaseConnectionRefusedException | DatabaseNotFoundException | RequirementsException | \PDOException $e) {
}
$source_id_field_names = array_keys($source_plugin->getIds());
$count = 1;
foreach ($source_id_field_names as $source_id_field_name) {
$display_name = preg_replace(
[
'/^[Tt]he /',
'/\.$/',
], '', $fields[$source_id_field_name] ?? $source_id_field_name);
$header[] = [
'data' => ucfirst($display_name),
'field' => 'sourceid' . $count++,
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
];
}
$header[] = [
'data' => $this->t('Severity level'),
'field' => 'level',
'class' => [RESPONSIVE_PRIORITY_LOW],
];
$header[] = [
'data' => $this->t('Message'),
'field' => 'message',
];
$levels = [
MigrationInterface::MESSAGE_ERROR => $this->t('Error'),
MigrationInterface::MESSAGE_WARNING => $this->t('Warning'),
MigrationInterface::MESSAGE_NOTICE => $this->t('Notice'),
MigrationInterface::MESSAGE_INFORMATIONAL => $this->t('Info'),
];
// Gets each message row and the source ID(s) for that message.
$query = $this->database->select($message_table, 'msg')
->extend('\Drupal\Core\Database\Query\PagerSelectExtender')
->extend('\Drupal\Core\Database\Query\TableSortExtender');
// Not all messages have a matching row in the map table.
$query->leftJoin($map_table, 'map', 'msg.source_ids_hash = map.source_ids_hash');
$query->fields('msg');
$query->fields('map');
$filter = $this->buildFilterQuery($request);
if (!empty($filter['where'])) {
$query->where($filter['where'], $filter['args']);
}
$result = $query
->limit(50)
->orderByHeader($header)
->execute();
// Build the rows to display.
$rows = [];
$add_explanation = FALSE;
$num_ids = count($source_id_field_names);
foreach ($result as $message_row) {
$new_row = [];
for ($count = 1; $count <= $num_ids; $count++) {
$map_key = 'sourceid' . $count;
$new_row[$map_key] = $message_row->$map_key ?? NULL;
if (empty($new_row[$map_key])) {
$new_row[$map_key] = $this->t('Not available');
$add_explanation = TRUE;
}
}
$new_row['level'] = $levels[$message_row->level];
$new_row['message'] = $message_row->message;
$rows[] = $new_row;
}
// Build the complete form.
$build['message_filter_form'] = $this->formBuilder->getForm('Drupal\migrate\Form\MessageForm');
$build['message_table'] = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No messages for this migration.'),
'#attributes' => ['id' => 'admin-migrate-msg', 'class' => ['admin-migrate-msg']],
];
$build['message_pager'] = ['#type' => 'pager'];
if ($add_explanation) {
$build['explanation'] = [
'#type' => 'item',
'#markup' => $this->t("When there is an error processing a row, the migration system saves the error message but not the source ID(s) of the row. That is why some messages in this table have 'Not available' in the source ID column(s)."),
];
}
return $build;
}
/**
* Builds a query for migrate message administration.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return array|null
* An associative array with keys 'where' and 'args' or NULL if there were
* no filters set.
*/
protected function buildFilterQuery(Request $request): ?array {
$session_filters = $request->getSession()->get('migration_messages_overview_filter', []);
if (empty($session_filters)) {
return NULL;
}
// Build query.
$where = $args = [];
foreach ($session_filters as $filter) {
$filter_where = [];
switch ($filter['type']) {
case 'array':
foreach ($filter['value'] as $value) {
$filter_where[] = $filter['where'];
$args[] = $value;
}
break;
case 'string':
$filter_where[] = $filter['where'];
$args[] = '%' . $filter['value'] . '%';
break;
default:
$filter_where[] = $filter['where'];
$args[] = $filter['value'];
}
if (!empty($filter_where)) {
$where[] = '(' . implode(' OR ', $filter_where) . ')';
}
}
$where = !empty($where) ? implode(' AND ', $where) : '';
return [
'where' => $where,
'args' => $args,
];
}
/**
* Gets the title for the details page.
*
* @param string $migration_id
* A migration ID.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The translated title.
*/
public function title(string $migration_id): TranslatableMarkup {
return $this->t(
'Messages of %migration',
['%migration' => $migration_id]
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\migrate;
/**
* The entity field definition trait.
*/
trait EntityFieldDefinitionTrait {
/**
* Gets the field definition from a specific entity base field.
*
* The method takes the field ID as an argument and returns the field storage
* definition to be used in getIds() by querying the destination entity base
* field definition.
*
* @param string $key
* The field ID key.
*
* @return array
* An associative array with a structure that contains the field type, keyed
* as 'type', together with field storage settings as they are returned by
* FieldStorageDefinitionInterface::getSettings().
*
* @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
*/
protected function getDefinitionFromEntity($key) {
$plugin_id = $this->getPluginId();
$entity_type_id = $this->getEntityTypeId($plugin_id);
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $definitions */
$definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
$field_definition = $definitions[$key];
return [
'type' => $field_definition->getType(),
] + $field_definition->getSettings();
}
/**
* Finds the entity type from configuration or plugin ID.
*
* @param string $plugin_id
* The plugin ID.
*
* @return string
* The entity type.
*/
protected static function getEntityTypeId($plugin_id) {
$entity_type_id = NULL;
if (strpos($plugin_id, static::DERIVATIVE_SEPARATOR)) {
[, $entity_type_id] = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 2);
}
return $entity_type_id;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\Component\EventDispatcher\Event;
class EventBase extends Event {
/**
* The migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The current message service.
*
* @var \Drupal\migrate\MigrateMessageInterface
*/
protected $message;
/**
* Constructs a Migrate event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration being run.
* @param \Drupal\migrate\MigrateMessageInterface $message
* The Migrate message service.
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message) {
$this->migration = $migration;
$this->message = $message;
}
/**
* Gets the migration.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration being run.
*/
public function getMigration() {
return $this->migration;
}
/**
* Logs a message using the Migrate message service.
*
* @param string $message
* The message to log.
* @param string $type
* The type of message, for example: status or warning.
*/
public function logMessage($message, $type = 'status') {
$this->message->display($message, $type);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate\Event;
/**
* Interface for plugins that react to pre- or post-import events.
*/
interface ImportAwareInterface {
/**
* Performs pre-import tasks.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The pre-import event object.
*/
public function preImport(MigrateImportEvent $event);
/**
* Performs post-import tasks.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The post-import event object.
*/
public function postImport(MigrateImportEvent $event);
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Drupal\migrate\Event;
// cspell:ignore idmap
/**
* Defines events for the migration system.
*
* @see \Drupal\migrate\Event\MigrateMapSaveEvent
* @see \Drupal\migrate\Event\MigrateMapDeleteEvent
* @see \Drupal\migrate\Event\MigrateImportEvent
* @see \Drupal\migrate\Event\MigratePreRowSaveEvent
* @see \Drupal\migrate\Event\MigratePostRowSaveEvent
* @see \Drupal\migrate\Event\MigrateRollbackEvent
* @see \Drupal\migrate\Event\MigrateRowDeleteEvent
* @see \Drupal\migrate\Event\MigrateIdMapMessageEvent
*/
final class MigrateEvents {
/**
* Name of the event fired when saving to a migration's map.
*
* This event allows modules to perform an action whenever the disposition of
* an item being migrated is saved to the map table. The event listener method
* receives a \Drupal\migrate\Event\MigrateMapSaveEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateMapSaveEvent
*
* @var string
*/
const MAP_SAVE = 'migrate.map_save';
/**
* Name of the event fired when removing an entry from a migration's map.
*
* This event allows modules to perform an action whenever a row is deleted
* from a migration's map table (implying it has been rolled back). The event
* listener method receives a \Drupal\migrate\Event\MigrateMapDeleteEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateMapDeleteEvent
*
* @var string
*/
const MAP_DELETE = 'migrate.map_delete';
/**
* Name of the event fired when beginning a migration import operation.
*
* This event allows modules to perform an action whenever a migration import
* operation is about to begin. The event listener method receives a
* \Drupal\migrate\Event\MigrateImportEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateImportEvent
*
* @var string
*/
const PRE_IMPORT = 'migrate.pre_import';
/**
* Name of the event fired when finishing a migration import operation.
*
* This event allows modules to perform an action whenever a migration import
* operation is completing. The event listener method receives a
* \Drupal\migrate\Event\MigrateImportEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateImportEvent
*
* @var string
*/
const POST_IMPORT = 'migrate.post_import';
/**
* Name of the event fired when about to import a single item.
*
* This event allows modules to perform an action whenever a specific item
* is about to be saved by the destination plugin. The event listener method
* receives a \Drupal\migrate\Event\MigratePreRowSaveEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigratePreRowSaveEvent
*
* @var string
*/
const PRE_ROW_SAVE = 'migrate.pre_row_save';
/**
* Name of the event fired just after a single item has been imported.
*
* This event allows modules to perform an action whenever a specific item
* has been saved by the destination plugin. The event listener method
* receives a \Drupal\migrate\Event\MigratePostRowSaveEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigratePostRowSaveEvent
*
* @var string
*/
const POST_ROW_SAVE = 'migrate.post_row_save';
/**
* Name of the event fired when beginning a migration rollback operation.
*
* This event allows modules to perform an action whenever a migration
* rollback operation is about to begin. The event listener method receives a
* \Drupal\migrate\Event\MigrateRollbackEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateRollbackEvent
*
* @var string
*/
const PRE_ROLLBACK = 'migrate.pre_rollback';
/**
* Name of the event fired when finishing a migration rollback operation.
*
* This event allows modules to perform an action whenever a migration
* rollback operation is completing. The event listener method receives a
* \Drupal\migrate\Event\MigrateRollbackEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateRollbackEvent
*
* @var string
*/
const POST_ROLLBACK = 'migrate.post_rollback';
/**
* Name of the event fired when about to delete a single item.
*
* This event allows modules to perform an action whenever a specific item
* is about to be deleted by the destination plugin. The event listener method
* receives a \Drupal\migrate\Event\MigrateRowDeleteEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateRowDeleteEvent
*
* @var string
*/
const PRE_ROW_DELETE = 'migrate.pre_row_delete';
/**
* Name of the event fired just after a single item has been deleted.
*
* This event allows modules to perform an action whenever a specific item
* has been deleted by the destination plugin. The event listener method
* receives a \Drupal\migrate\Event\MigrateRowDeleteEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateRowDeleteEvent
*
* @var string
*/
const POST_ROW_DELETE = 'migrate.post_row_delete';
/**
* Name of the event fired when saving a message to the ID map.
*
* This event allows modules to perform an action whenever a message is being
* logged by the ID map. The event listener method receives a
* \Drupal\migrate\Event\MigrateIdMapMessageEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateIdMapMessageEvent
*
* @var string
*/
const IDMAP_MESSAGE = 'migrate.idmap_message';
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Wraps an ID map message event for event listeners.
*/
class MigrateIdMapMessageEvent extends Event {
/**
* Migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Array of values uniquely identifying the source row.
*
* @var array
*/
protected $sourceIdValues;
/**
* Message to be logged.
*
* @var string
*/
protected $message;
/**
* Message severity.
*
* @var int
*/
protected $level;
/**
* Constructs a post-save event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
* @param array $source_id_values
* Values represent the source ID.
* @param string $message
* The message
* @param int $level
* Severity level (one of the MigrationInterface::MESSAGE_* constants).
*/
public function __construct(MigrationInterface $migration, array $source_id_values, $message, $level) {
$this->migration = $migration;
$this->sourceIdValues = $source_id_values;
$this->message = $message;
$this->level = $level;
}
/**
* Gets the migration entity.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration entity involved.
*/
public function getMigration() {
return $this->migration;
}
/**
* Gets the source ID values.
*
* @return array
* The source ID as an array.
*/
public function getSourceIdValues() {
return $this->sourceIdValues;
}
/**
* Gets the message to be logged.
*
* @return string
* The message text.
*/
public function getMessage() {
return $this->message;
}
/**
* Gets the severity level of the message.
*
* Message levels are declared in MigrationInterface and start with MESSAGE_.
*
* @see \Drupal\migrate\Plugin\MigrationInterface
*
* @return int
* The message level.
*/
public function getLevel() {
return $this->level;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Drupal\migrate\Event;
/**
* Wraps a pre- or post-import event for event listeners.
*/
class MigrateImportEvent extends EventBase {}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a migrate map delete event for event listeners.
*/
class MigrateMapDeleteEvent extends Event {
/**
* Map plugin.
*
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $map;
/**
* Array of source ID fields.
*
* @var array
*/
protected $sourceId;
/**
* Constructs a migration map delete event object.
*
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $map
* Map plugin.
* @param array $source_id
* Array of source ID fields representing the object being deleted from the map.
*/
public function __construct(MigrateIdMapInterface $map, array $source_id) {
$this->map = $map;
$this->sourceId = $source_id;
}
/**
* Gets the map plugin.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The map plugin that caused the event to fire.
*/
public function getMap() {
return $this->map;
}
/**
* Gets the source ID of the item being removed from the map.
*
* @return array
* Array of source ID fields.
*/
public function getSourceId() {
return $this->sourceId;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a migrate map save event for event listeners.
*/
class MigrateMapSaveEvent extends Event {
/**
* Map plugin.
*
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $map;
/**
* Array of fields being saved to the map, keyed by field name.
*
* @var array
*/
protected $fields;
/**
* Constructs a migration map event object.
*
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $map
* Map plugin.
* @param array $fields
* Array of fields being saved to the map.
*/
public function __construct(MigrateIdMapInterface $map, array $fields) {
$this->map = $map;
$this->fields = $fields;
}
/**
* Gets the map plugin.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The map plugin that caused the event to fire.
*/
public function getMap() {
return $this->map;
}
/**
* Gets the fields about to be saved to the map.
*
* @return array
* Array of map fields, keyed by field name.
*/
public function getFields() {
return $this->fields;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Row;
/**
* Wraps a post-save event for event listeners.
*/
class MigratePostRowSaveEvent extends MigratePreRowSaveEvent {
/**
* The row's destination ID.
*
* @var array|bool
*/
protected $destinationIdValues = [];
/**
* Constructs a post-save event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
* @param \Drupal\migrate\MigrateMessageInterface $message
* The message interface.
* @param \Drupal\migrate\Row $row
* Row object.
* @param array|bool $destination_id_values
* Values represent the destination ID.
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, Row $row, $destination_id_values) {
parent::__construct($migration, $message, $row);
$this->destinationIdValues = $destination_id_values;
}
/**
* Gets the destination ID values.
*
* @return array
* The destination ID as an array.
*/
public function getDestinationIdValues() {
return $this->destinationIdValues;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Row;
/**
* Wraps a pre-save event for event listeners.
*/
class MigratePreRowSaveEvent extends EventBase {
/**
* Row object.
*
* @var \Drupal\migrate\Row
*/
protected $row;
/**
* Constructs a pre-save event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
* @param \Drupal\migrate\MigrateMessageInterface $message
* The current migrate message service.
* @param \Drupal\migrate\Row $row
* The current row.
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, Row $row) {
parent::__construct($migration, $message);
$this->row = $row;
}
/**
* Gets the row object.
*
* @return \Drupal\migrate\Row
* The row object about to be imported.
*/
public function getRow() {
return $this->row;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a pre- or post-rollback event for event listeners.
*/
class MigrateRollbackEvent extends Event {
/**
* Migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Constructs a rollback event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
*/
public function __construct(MigrationInterface $migration) {
$this->migration = $migration;
}
/**
* Gets the migration entity.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration entity involved.
*/
public function getMigration() {
return $this->migration;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a row deletion event for event listeners.
*/
class MigrateRowDeleteEvent extends Event {
/**
* Migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Values representing the destination ID.
*
* @var array
*/
protected $destinationIdValues;
/**
* Constructs a row deletion event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
* @param array $destination_id_values
* Values represent the destination ID.
*/
public function __construct(MigrationInterface $migration, $destination_id_values) {
$this->migration = $migration;
$this->destinationIdValues = $destination_id_values;
}
/**
* Gets the migration entity.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration being rolled back.
*/
public function getMigration() {
return $this->migration;
}
/**
* Gets the destination ID values.
*
* @return array
* The destination ID as an array.
*/
public function getDestinationIdValues() {
return $this->destinationIdValues;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate\Event;
/**
* Interface for plugins that react to pre- or post-rollback events.
*/
interface RollbackAwareInterface {
/**
* Performs pre-rollback tasks.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The pre-rollback event object.
*/
public function preRollback(MigrateRollbackEvent $event);
/**
* Performs post-rollback tasks.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The post-rollback event object.
*/
public function postRollback(MigrateRollbackEvent $event);
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Drupal\migrate\Exception;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\migrate\MigrateException;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* To throw when an entity generated during the import is not valid.
*/
class EntityValidationException extends MigrateException {
/**
* The separator for combining multiple messages into a single string.
*
* Afterwards, the separator could be used to split a concatenated string
* onto multiple lines.
*
* @code
* explode(EntityValidationException::MESSAGES_SEPARATOR, $messages);
* @endcode
*/
const MESSAGES_SEPARATOR = '||';
/**
* The list of violations generated during the entity validation.
*
* @var \Drupal\Core\Entity\EntityConstraintViolationListInterface
*/
protected $violations;
/**
* EntityValidationException constructor.
*
* @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
* The list of violations generated during the entity validation.
*/
public function __construct(EntityConstraintViolationListInterface $violations) {
$this->violations = $violations;
$entity = $this->violations->getEntity();
$locator = $entity->getEntityTypeId();
if ($entity_id = $entity->id()) {
$locator = sprintf('%s: %s', $locator, $entity_id);
if ($entity instanceof RevisionableInterface && $revision_id = $entity->getRevisionId()) {
$locator .= sprintf(', revision: %s', $revision_id);
}
}
// Example: "[user]: field_a=Violation 1., field_b=Violation 2.".
// Example: "[user: 1]: field_a=Violation 1., field_b=Violation 2.".
// Example: "[node: 19, revision: 12129]: field_a=Violation 1.".
parent::__construct(sprintf('[%s]: %s', $locator, implode(static::MESSAGES_SEPARATOR, $this->getViolationMessages())));
}
/**
* Returns the list of violation messages.
*
* @return string[]
* The list of violation messages.
*/
public function getViolationMessages() {
$messages = [];
foreach ($this->violations as $violation) {
assert($violation instanceof ConstraintViolationInterface);
$messages[] = sprintf('%s=%s', $violation->getPropertyPath(), $violation->getMessage());
}
return $messages;
}
/**
* Returns the list of violations generated during the entity validation.
*
* @return \Drupal\Core\Entity\EntityConstraintViolationListInterface
* The list of violations generated during the entity validation.
*/
public function getViolations() {
return $this->violations;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Drupal\migrate\Exception;
/**
* Defines an exception thrown when a migration does not meet the requirements.
*
* @see \Drupal\migrate\Plugin\RequirementsInterface
*/
class RequirementsException extends \RuntimeException {
/**
* The missing requirements.
*
* @var array
*/
protected $requirements;
/**
* Constructs a new RequirementsException instance.
*
* @param string $message
* (optional) The Exception message to throw.
* @param array $requirements
* (optional) The missing requirements.
* @param int $code
* (optional) The Exception code.
* @param \Exception $previous
* (optional) The previous exception used for the exception chaining.
*/
public function __construct($message = "", array $requirements = [], $code = 0, ?\Exception $previous = NULL) {
parent::__construct($message, $code, $previous);
$this->requirements = $requirements;
}
/**
* Get an array of requirements.
*
* @return array
* The requirements.
*/
public function getRequirements() {
return $this->requirements;
}
/**
* Get the requirements as a string.
*
* @return string
* A formatted requirements string.
*/
public function getRequirementsString() {
$output = '';
foreach ($this->requirements as $requirement_type => $requirements) {
if (!is_array($requirements)) {
$requirements = [$requirements];
}
foreach ($requirements as $value) {
$output .= "$requirement_type: $value. ";
}
}
return trim($output);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Drupal\migrate\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Migrate messages form.
*
* @internal
*/
class MessageForm extends FormBase {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$form = new static();
$form->setStringTranslation($container->get('string_translation'));
return $form;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'migrate_messages_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$session_filters = $this->getRequest()->getSession()->get('migration_messages_overview_filter', []);
$form['filters'] = [
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Filter messages'),
'#weight' => 0,
];
$form['filters']['message'] = [
'#type' => 'textfield',
'#title' => $this->t('Message'),
'#default_value' => $session_filters['message']['value'] ?? '',
];
$form['filters']['severity'] = [
'#type' => 'select',
'#title' => $this->t('Severity level'),
'#default_value' => $session_filters['severity']['value'] ?? [],
'#options' => [
MigrationInterface::MESSAGE_ERROR => $this->t('Error'),
MigrationInterface::MESSAGE_WARNING => $this->t('Warning'),
MigrationInterface::MESSAGE_NOTICE => $this->t('Notice'),
MigrationInterface::MESSAGE_INFORMATIONAL => $this->t('Info'),
],
'#multiple' => TRUE,
'#size' => 4,
];
$form['filters']['actions'] = [
'#type' => 'actions',
'#attributes' => ['class' => ['container-inline']],
];
$form['filters']['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Filter'),
];
$form['filters']['actions']['reset'] = [
'#type' => 'submit',
'#value' => $this->t('Reset'),
'#submit' => ['::resetForm'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$filters['message'] = [
'title' => $this->t('message'),
'where' => 'msg.message LIKE ?',
'type' => 'string',
];
$filters['severity'] = [
'title' => $this->t('Severity'),
'where' => 'msg.level = ?',
'type' => 'array',
];
$session_filters = $this->getRequest()->getSession()->get('migration_messages_overview_filter', []);
foreach ($filters as $name => $filter) {
if ($form_state->hasValue($name)) {
$session_filters[$name] = [
'where' => $filter['where'],
'value' => $form_state->getValue($name),
'type' => $filter['type'],
];
}
}
$this->getRequest()->getSession()->set('migration_messages_overview_filter', $session_filters);
}
/**
* Resets the filter form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function resetForm(array $form, FormStateInterface $form_state): void {
$this->getRequest()->getSession()->remove('migration_messages_overview_filter');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\migrate;
interface MigrateBuildDependencyInterface {
/**
* Builds a dependency tree for the migrations and set their order.
*
* @param \Drupal\migrate\Plugin\MigrationInterface[] $migrations
* Array of loaded migrations with their declared dependencies.
* @param array $dynamic_ids
* Keys are dynamic ids (for example node:*) values are a list of loaded
* migration ids (for example node:page, node:article).
*
* @return array
* An array of migrations.
*/
public function buildDependencyMigration(array $migrations, array $dynamic_ids);
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\migrate;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Defines the migrate exception class.
*/
class MigrateException extends \Exception {
/**
* The level of the error being reported.
*
* The value is a MigrationInterface::MESSAGE_* constant.
*
* @var int
*
* @see \Drupal\migrate\Plugin\MigrationInterface
*/
protected $level;
/**
* The status to record in the map table for the current item.
*
* The value is a MigrateIdMapInterface::STATUS_* constant.
*
* @var int
*
* @see \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $status;
/**
* Constructs a MigrateException object.
*
* @param string $message
* The message for the exception.
* @param int $code
* The Exception code.
* @param \Exception $previous
* The previous exception used for the exception chaining.
* @param int $level
* The level of the error, a Migration::MESSAGE_* constant.
* @param int $status
* The status of the item for the map table, a MigrateMap::STATUS_*
* constant.
*/
public function __construct($message = '', $code = 0, ?\Exception $previous = NULL, $level = MigrationInterface::MESSAGE_ERROR, $status = MigrateIdMapInterface::STATUS_FAILED) {
$this->level = $level;
$this->status = $status;
parent::__construct($message);
}
/**
* Gets the level.
*
* @return int
* An integer status code. @see Migration::MESSAGE_*
*/
public function getLevel() {
return $this->level;
}
/**
* Gets the status of the current item.
*
* @return int
* An integer status code. @see MigrateMap::STATUS_*
*/
public function getStatus() {
return $this->status;
}
}

View File

@@ -0,0 +1,643 @@
<?php
namespace Drupal\migrate;
use Drupal\Component\Utility\Bytes;
use Drupal\Core\StringTranslation\ByteSizeMarkup;
use Drupal\Core\Utility\Error;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\Event\MigrateRowDeleteEvent;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Defines a migrate executable class.
*/
class MigrateExecutable implements MigrateExecutableInterface {
use StringTranslationTrait;
/**
* The configuration of the migration to do.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Status of one row.
*
* The value is a MigrateIdMapInterface::STATUS_* constant, for example:
* STATUS_IMPORTED.
*
* @var int
*/
protected $sourceRowStatus;
/**
* The ratio of the memory limit at which an operation will be interrupted.
*
* @var float
*/
protected $memoryThreshold = 0.85;
/**
* The PHP memory_limit expressed in bytes.
*
* @var int
*/
protected $memoryLimit;
/**
* The configuration values of the source.
*
* @var array
*/
protected $sourceIdValues;
/**
* An array of counts. Initially used for cache hit/miss tracking.
*
* @var array
*/
protected $counts = [];
/**
* The source.
*
* @var \Drupal\migrate\Plugin\MigrateSourceInterface
*/
protected $source;
/**
* The event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Migration message service.
*
* @todo https://www.drupal.org/node/2822663 Make this protected.
*
* @var \Drupal\migrate\MigrateMessageInterface
*/
public $message;
/**
* Constructs a MigrateExecutable and verifies and sets the memory limit.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to run.
* @param \Drupal\migrate\MigrateMessageInterface $message
* (optional) The migrate message service.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* (optional) The event dispatcher.
*/
public function __construct(MigrationInterface $migration, ?MigrateMessageInterface $message = NULL, ?EventDispatcherInterface $event_dispatcher = NULL) {
$this->migration = $migration;
$this->message = $message ?: new MigrateMessage();
$this->getIdMap()->setMessage($this->message);
$this->eventDispatcher = $event_dispatcher;
// Record the memory limit in bytes
$limit = trim(ini_get('memory_limit'));
if ($limit == '-1') {
$this->memoryLimit = PHP_INT_MAX;
}
else {
$this->memoryLimit = Bytes::toNumber($limit);
}
}
/**
* Returns the source.
*
* Makes sure source is initialized based on migration settings.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface
* The source.
*/
protected function getSource() {
if (!isset($this->source)) {
$this->source = $this->migration->getSourcePlugin();
}
return $this->source;
}
/**
* Gets the event dispatcher.
*
* @return \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected function getEventDispatcher() {
if (!$this->eventDispatcher) {
$this->eventDispatcher = \Drupal::service('event_dispatcher');
}
return $this->eventDispatcher;
}
/**
* {@inheritdoc}
*/
public function import() {
// Only begin the import operation if the migration is currently idle.
if ($this->migration->getStatus() !== MigrationInterface::STATUS_IDLE) {
$this->message->display($this->t('Migration @id is busy with another operation: @status',
[
'@id' => $this->migration->id(),
'@status' => $this->t($this->migration->getStatusLabel()),
]), 'error');
return MigrationInterface::RESULT_FAILED;
}
$this->getEventDispatcher()->dispatch(new MigrateImportEvent($this->migration, $this->message), MigrateEvents::PRE_IMPORT);
// Knock off migration if the requirements haven't been met.
try {
$this->migration->checkRequirements();
}
catch (RequirementsException $e) {
$this->message->display(
$this->t(
'Migration @id did not meet the requirements. @message',
[
'@id' => $this->migration->id(),
'@message' => $e->getMessage(),
]
),
'error'
);
return MigrationInterface::RESULT_FAILED;
}
$this->migration->setStatus(MigrationInterface::STATUS_IMPORTING);
$source = $this->getSource();
try {
$source->rewind();
}
catch (\Exception $e) {
$this->message->display(
$this->t('Migration failed with source plugin exception: @e in @file line @line', [
'@e' => $e->getMessage(),
'@file' => $e->getFile(),
'@line' => $e->getLine(),
]), 'error');
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
return MigrationInterface::RESULT_FAILED;
}
// Get the process pipeline.
$pipeline = FALSE;
if ($source->valid()) {
try {
$pipeline = $this->migration->getProcessPlugins();
}
catch (MigrateException $e) {
$row = $source->current();
$this->sourceIdValues = $row->getSourceIdValues();
$this->getIdMap()->saveIdMapping($row, [], $e->getStatus());
$this->saveMessage($e->getMessage(), $e->getLevel());
}
}
$return = MigrationInterface::RESULT_COMPLETED;
if ($pipeline) {
$id_map = $this->getIdMap();
$destination = $this->migration->getDestinationPlugin();
while ($source->valid()) {
$row = $source->current();
$this->sourceIdValues = $row->getSourceIdValues();
try {
foreach ($pipeline as $destination_property_name => $plugins) {
$this->processPipeline($row, $destination_property_name, $plugins, NULL);
}
$save = TRUE;
}
catch (MigrateException $e) {
$this->getIdMap()->saveIdMapping($row, [], $e->getStatus());
$msg = sprintf("%s:%s:%s", $this->migration->getPluginId(), $destination_property_name, $e->getMessage());
$this->saveMessage($msg, $e->getLevel());
$save = FALSE;
}
catch (MigrateSkipRowException $e) {
if ($e->getSaveToMap()) {
$id_map->saveIdMapping($row, [], MigrateIdMapInterface::STATUS_IGNORED);
}
if ($message = trim($e->getMessage())) {
$msg = sprintf("%s:%s: %s", $this->migration->getPluginId(), $destination_property_name, $message);
$this->saveMessage($msg, MigrationInterface::MESSAGE_INFORMATIONAL);
}
$save = FALSE;
}
if ($save) {
try {
$this->getEventDispatcher()
->dispatch(new MigratePreRowSaveEvent($this->migration, $this->message, $row), MigrateEvents::PRE_ROW_SAVE);
$destination_ids = $id_map->lookupDestinationIds($this->sourceIdValues);
$destination_id_values = $destination_ids ? reset($destination_ids) : [];
$destination_id_values = $destination->import($row, $destination_id_values);
$this->getEventDispatcher()
->dispatch(new MigratePostRowSaveEvent($this->migration, $this->message, $row, $destination_id_values), MigrateEvents::POST_ROW_SAVE);
if ($destination_id_values) {
// We do not save an idMap entry for config.
if ($destination_id_values !== TRUE) {
$id_map->saveIdMapping($row, $destination_id_values, $this->sourceRowStatus, $destination->rollbackAction());
}
}
else {
$id_map->saveIdMapping($row, [], MigrateIdMapInterface::STATUS_FAILED);
if (!$id_map->messageCount()) {
$message = $this->t('New object was not saved, no error provided');
$this->saveMessage($message);
$this->message->display($message);
}
}
}
catch (MigrateException $e) {
$this->getIdMap()->saveIdMapping($row, [], $e->getStatus());
$this->saveMessage($e->getMessage(), $e->getLevel());
}
catch (\Exception $e) {
$this->getIdMap()
->saveIdMapping($row, [], MigrateIdMapInterface::STATUS_FAILED);
$this->handleException($e);
}
}
$this->sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
// Check for memory exhaustion.
if (($return = $this->checkStatus()) != MigrationInterface::RESULT_COMPLETED) {
break;
}
// If anyone has requested we stop, return the requested result.
if ($this->migration->getStatus() == MigrationInterface::STATUS_STOPPING) {
$return = $this->migration->getInterruptionResult();
$this->migration->clearInterruptionResult();
break;
}
try {
$source->next();
}
catch (\Exception $e) {
$this->message->display(
$this->t('Migration failed with source plugin exception: @e in @file line @line', [
'@e' => $e->getMessage(),
'@file' => $e->getFile(),
'@line' => $e->getLine(),
]), 'error');
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
return MigrationInterface::RESULT_FAILED;
}
}
}
$this->getEventDispatcher()->dispatch(new MigrateImportEvent($this->migration, $this->message), MigrateEvents::POST_IMPORT);
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
return $return;
}
/**
* {@inheritdoc}
*/
public function rollback() {
// Only begin the rollback operation if the migration is currently idle.
if ($this->migration->getStatus() !== MigrationInterface::STATUS_IDLE) {
$this->message->display($this->t('Migration @id is busy with another operation: @status', ['@id' => $this->migration->id(), '@status' => $this->t($this->migration->getStatusLabel())]), 'error');
return MigrationInterface::RESULT_FAILED;
}
// Announce that rollback is about to happen.
$this->getEventDispatcher()->dispatch(new MigrateRollbackEvent($this->migration), MigrateEvents::PRE_ROLLBACK);
// Optimistically assume things are going to work out; if not, $return will be
// updated to some other status.
$return = MigrationInterface::RESULT_COMPLETED;
$this->migration->setStatus(MigrationInterface::STATUS_ROLLING_BACK);
$id_map = $this->getIdMap();
$destination = $this->migration->getDestinationPlugin();
// Loop through each row in the map, and try to roll it back.
$id_map->rewind();
while ($id_map->valid()) {
$destination_key = $id_map->currentDestination();
if ($destination_key) {
$map_row = $id_map->getRowByDestination($destination_key);
if (!isset($map_row['rollback_action']) || $map_row['rollback_action'] == MigrateIdMapInterface::ROLLBACK_DELETE) {
$this->getEventDispatcher()
->dispatch(new MigrateRowDeleteEvent($this->migration, $destination_key), MigrateEvents::PRE_ROW_DELETE);
$destination->rollback($destination_key);
$this->getEventDispatcher()
->dispatch(new MigrateRowDeleteEvent($this->migration, $destination_key), MigrateEvents::POST_ROW_DELETE);
}
// We're now done with this row, so remove it from the map.
$id_map->deleteDestination($destination_key);
}
else {
// If there is no destination key the import probably failed and we can
// remove the row without further action.
$source_key = $id_map->currentSource();
$id_map->delete($source_key);
}
$id_map->next();
// Check for memory exhaustion.
if (($return = $this->checkStatus()) != MigrationInterface::RESULT_COMPLETED) {
break;
}
// If anyone has requested we stop, return the requested result.
if ($this->migration->getStatus() == MigrationInterface::STATUS_STOPPING) {
$return = $this->migration->getInterruptionResult();
$this->migration->clearInterruptionResult();
break;
}
}
// Notify modules that rollback attempt was complete.
$this->getEventDispatcher()->dispatch(new MigrateRollbackEvent($this->migration), MigrateEvents::POST_ROLLBACK);
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
return $return;
}
/**
* Get the ID map from the current migration.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The ID map.
*/
protected function getIdMap() {
return $this->migration->getIdMap();
}
/**
* {@inheritdoc}
*/
public function processRow(Row $row, ?array $process = NULL, $value = NULL) {
foreach ($this->migration->getProcessPlugins($process) as $destination => $plugins) {
$this->processPipeline($row, $destination, $plugins, $value);
}
}
/**
* Runs a process pipeline.
*
* @param \Drupal\migrate\Row $row
* The $row to be processed.
* @param string $destination
* The destination property name.
* @param array $plugins
* The process pipeline plugins.
* @param mixed $value
* (optional) Initial value of the pipeline for the destination.
*
* @see \Drupal\migrate\MigrateExecutableInterface::processRow
*
* @throws \Drupal\migrate\MigrateException
*/
protected function processPipeline(Row $row, string $destination, array $plugins, $value) {
$multiple = FALSE;
/** @var \Drupal\migrate\Plugin\MigrateProcessInterface $plugin */
foreach ($plugins as $plugin) {
$definition = $plugin->getPluginDefinition();
// Many plugins expect a scalar value but the current value of the
// pipeline might be multiple scalars (this is set by the previous plugin)
// and in this case the current value needs to be iterated and each scalar
// separately transformed.
if ($multiple && !$definition['handle_multiples']) {
$new_value = [];
if (!is_array($value)) {
throw new MigrateException(sprintf('Pipeline failed at %s plugin for destination %s: %s received instead of an array,', $plugin->getPluginId(), $destination, $value));
}
$break = FALSE;
foreach ($value as $scalar_value) {
$plugin->reset();
try {
$new_value[] = $plugin->transform($scalar_value, $this, $row, $destination);
}
catch (MigrateSkipProcessException $e) {
$new_value[] = NULL;
$break = TRUE;
}
catch (MigrateException $e) {
// Prepend the process plugin id to the message.
$message = sprintf("%s: %s", $plugin->getPluginId(), $e->getMessage());
throw new MigrateException($message);
}
if ($plugin->isPipelineStopped()) {
$break = TRUE;
}
}
$value = $new_value;
if ($break) {
break;
}
}
else {
$plugin->reset();
try {
$value = $plugin->transform($value, $this, $row, $destination);
}
catch (MigrateSkipProcessException $e) {
$value = NULL;
break;
}
catch (MigrateException $e) {
// Prepend the process plugin id to the message.
$message = sprintf("%s: %s", $plugin->getPluginId(), $e->getMessage());
throw new MigrateException($message);
}
if ($plugin->isPipelineStopped()) {
break;
}
$multiple = $plugin->multiple();
}
}
// Ensure all values, including nulls, are migrated.
if ($plugins) {
if (isset($value)) {
$row->setDestinationProperty($destination, $value);
}
else {
$row->setEmptyDestinationProperty($destination);
}
}
}
/**
* Fetches the key array for the current source record.
*
* @return array
* The current source IDs.
*/
protected function currentSourceIds() {
return $this->getSource()->getCurrentIds();
}
/**
* {@inheritdoc}
*/
public function saveMessage($message, $level = MigrationInterface::MESSAGE_ERROR) {
$this->getIdMap()->saveMessage($this->sourceIdValues, $message, $level);
}
/**
* Takes an Exception object and both saves and displays it.
*
* Pulls in additional information on the location triggering the exception.
*
* @param \Exception $exception
* Object representing the exception.
* @param bool $save
* (optional) Whether to save the message in the migration's mapping table.
* Set to FALSE in contexts where this doesn't make sense.
*/
protected function handleException(\Exception $exception, $save = TRUE) {
$result = Error::decodeException($exception);
$message = $result['@message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')';
if ($save) {
$this->saveMessage($message);
}
$this->message->display($message, 'error');
}
/**
* Checks for exceptional conditions, and display feedback.
*/
protected function checkStatus() {
if ($this->memoryExceeded()) {
return MigrationInterface::RESULT_INCOMPLETE;
}
return MigrationInterface::RESULT_COMPLETED;
}
/**
* Tests whether we've exceeded the desired memory threshold.
*
* If so, output a message.
*
* @return bool
* TRUE if the threshold is exceeded, otherwise FALSE.
*/
protected function memoryExceeded() {
$usage = $this->getMemoryUsage();
$pct_memory = $usage / $this->memoryLimit;
if (!$threshold = $this->memoryThreshold) {
return FALSE;
}
if ($pct_memory > $threshold) {
$this->message->display(
$this->t(
'Memory usage is @usage (@pct% of limit @limit), reclaiming memory.',
[
'@pct' => round($pct_memory * 100),
'@usage' => ByteSizeMarkup::create($usage, NULL, $this->stringTranslation),
'@limit' => ByteSizeMarkup::create($this->memoryLimit, NULL, $this->stringTranslation),
]
),
'warning'
);
$usage = $this->attemptMemoryReclaim();
$pct_memory = $usage / $this->memoryLimit;
// Use a lower threshold - we don't want to be in a situation where we keep
// coming back here and trimming a tiny amount
if ($pct_memory > (0.90 * $threshold)) {
$this->message->display(
$this->t(
'Memory usage is now @usage (@pct% of limit @limit), not enough reclaimed, starting new batch',
[
'@pct' => round($pct_memory * 100),
'@usage' => ByteSizeMarkup::create($usage, NULL, $this->stringTranslation),
'@limit' => ByteSizeMarkup::create($this->memoryLimit, NULL, $this->stringTranslation),
]
),
'warning'
);
return TRUE;
}
else {
$this->message->display(
$this->t(
'Memory usage is now @usage (@pct% of limit @limit), reclaimed enough, continuing',
[
'@pct' => round($pct_memory * 100),
'@usage' => ByteSizeMarkup::create($usage, NULL, $this->stringTranslation),
'@limit' => ByteSizeMarkup::create($this->memoryLimit, NULL, $this->stringTranslation),
]
),
'warning');
return FALSE;
}
}
else {
return FALSE;
}
}
/**
* Returns the memory usage so far.
*
* @return int
* The memory usage.
*/
protected function getMemoryUsage() {
return memory_get_usage();
}
/**
* Tries to reclaim memory.
*
* @return int
* The memory usage after reclaim.
*/
protected function attemptMemoryReclaim() {
// First, try resetting Drupal's static storage - this frequently releases
// plenty of memory to continue.
drupal_static_reset();
// Entity storage can blow up with caches, so clear it out.
\Drupal::service('entity.memory_cache')->deleteAll();
// @todo Explore resetting the container.
// Run garbage collector to further reduce memory.
gc_collect_cycles();
return memory_get_usage();
}
/**
* Generates a string representation for the given byte count.
*
* @param int $size
* A size in bytes.
*
* @return string
* A translated string representation of the size.
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use
* \Drupal\Core\StringTranslation\ByteSizeMarkup::create($size, $langcode)
* instead.
*
* @see https://www.drupal.org/node/2999981
*/
protected function formatSize($size) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use \Drupal\Core\StringTranslation\ByteSizeMarkup::create($size, $langcode) instead. See https://www.drupal.org/node/2999981', E_USER_DEPRECATED);
return format_size($size);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\migrate;
use Drupal\migrate\Plugin\MigrationInterface;
interface MigrateExecutableInterface {
/**
* Performs an import operation - migrate items from source to destination.
*
* @return int
* Returns a value indicating the status of the import operation.
* The possible values are the 'RESULT_' constants defined
* in MigrationInterface.
*
* @see \Drupal\migrate\Plugin\MigrationInterface
*/
public function import();
/**
* Performs a rollback operation - remove previously-imported items.
*/
public function rollback();
/**
* Processes a row.
*
* @param \Drupal\migrate\Row $row
* The $row to be processed.
* @param array $process
* (optional) A process pipeline configuration. If not set, the top level
* process configuration in the migration entity is used.
* @param mixed $value
* (optional) Initial value of the pipeline for the first destination.
* Usually setting this is not necessary as $process typically starts with
* a 'get'. This is useful only when the $process contains a single
* destination and needs to access a value outside of the source. See
* \Drupal\migrate\Plugin\migrate\process\SubProcess::transformKey for an
* example.
*
* @throws \Drupal\migrate\MigrateException
*/
public function processRow(Row $row, ?array $process = NULL, $value = NULL);
/**
* Passes messages through to the map class.
*
* @param string $message
* The message to record.
* @param int $level
* (optional) Message severity (defaults to MESSAGE_ERROR).
*/
public function saveMessage($message, $level = MigrationInterface::MESSAGE_ERROR);
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\migrate;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
/**
* Provides a migration lookup service.
*/
class MigrateLookup implements MigrateLookupInterface {
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* Constructs a MigrateLookup object.
*
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
*/
public function __construct(MigrationPluginManagerInterface $migration_plugin_manager) {
$this->migrationPluginManager = $migration_plugin_manager;
}
/**
* {@inheritdoc}
*/
public function lookup($migration_id, array $source_id_values) {
$results = [];
$migrations = $this->migrationPluginManager->createInstances($migration_id);
if (!$migrations) {
if (is_array($migration_id)) {
if (count($migration_id) != 1) {
throw new PluginException("Plugin IDs '" . implode("', '", $migration_id) . "' were not found.");
}
$migration_id = reset($migration_id);
}
throw new PluginNotFoundException($migration_id);
}
foreach ($migrations as $migration) {
if ($result = $this->doLookup($migration, $source_id_values)) {
$results = array_merge($results, $result);
}
}
return $results;
}
/**
* Performs a lookup.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration upon which to perform the lookup.
* @param array $source_id_values
* The source ID values to look up.
*
* @return array
* An array of arrays of destination identifier values.
*
* @throws \Drupal\migrate\MigrateException
* Thrown when $source_id_values contains unknown keys, or the wrong number
* of keys.
*/
protected function doLookup(MigrationInterface $migration, array $source_id_values) {
$destination_keys = array_keys($migration->getDestinationPlugin()->getIds());
$indexed_ids = $migration->getIdMap()
->lookupDestinationIds($source_id_values);
$keyed_ids = [];
foreach ($indexed_ids as $id) {
$keyed_ids[] = array_combine($destination_keys, $id);
}
return $keyed_ids;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\migrate;
/**
* Provides an interface for the migration lookup service.
*
* @package Drupal\migrate
*/
interface MigrateLookupInterface {
/**
* Retrieves destination ids from a migration lookup.
*
* @param string|string[] $migration_ids
* An array of migration plugin IDs to look up, or a single ID as a string.
* @param array $source_id_values
* An array of source id values.
*
* @return array
* An array of arrays of destination ids, or an empty array if none were
* found.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown by the migration plugin manager on error, or if the migration(s)
* cannot be found.
* @throws \Drupal\migrate\MigrateException
* Thrown when $source_id_values contains unknown keys, or is the wrong
* length.
*/
public function lookup($migration_ids, array $source_id_values);
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\migrate;
use Drupal\Core\Logger\RfcLogLevel;
/**
* Defines a migrate message class.
*/
class MigrateMessage implements MigrateMessageInterface {
/**
* The map between migrate status and watchdog severity.
*
* @var array
*/
protected $map = [
'status' => RfcLogLevel::INFO,
'error' => RfcLogLevel::ERROR,
];
/**
* {@inheritdoc}
*/
public function display($message, $type = 'status') {
$type = $this->map[$type] ?? RfcLogLevel::NOTICE;
\Drupal::logger('migrate')->log($type, $message);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Drupal\migrate;
interface MigrateMessageInterface {
/**
* Displays a migrate message.
*
* @param string $message
* The message to display.
* @param string $type
* The type of message, for example: status or warning.
*/
public function display($message, $type = 'status');
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\migrate;
/**
* This exception is thrown when the rest of the process should be skipped.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Return FALSE from a process
* plugin's isPipelineStopped() method to stop further processing on a
* pipeline.
* @see https://www.drupal.org/node/3414511
*/
class MigrateSkipProcessException extends \Exception {
public function __construct(string $message = "", int $code = 0, ?\Throwable $previous = NULL) {
trigger_error(__CLASS__ . " is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Return TRUE from a process plugin's isPipelineStopped() method to halt further processing on a pipeline. See https://www.drupal.org/node/3414511", E_USER_DEPRECATED);
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\migrate;
/**
* This exception is thrown when a row should be skipped.
*
* This exception should be used in Migrate process plugins. Throwing it in a
* source plugin may cause unexpected results in the count of rows processed.
* And throwing it in a destination plugin causes an error.
*/
class MigrateSkipRowException extends \Exception {
/**
* Whether to record the skip in the map table, or skip silently.
*
* @var bool
* TRUE to record as STATUS_IGNORED in the map, FALSE to skip silently.
*/
protected $saveToMap;
/**
* Constructs a MigrateSkipRowException object.
*
* @param string $message
* The message for the exception.
* @param bool $save_to_map
* TRUE to record as STATUS_IGNORED in the map, FALSE to skip silently.
*/
public function __construct($message = '', $save_to_map = TRUE) {
parent::__construct($message);
$this->saveToMap = $save_to_map;
}
/**
* Whether the thrower wants to record this skip in the map table.
*
* @return bool
* TRUE to record as STATUS_IGNORED in the map, FALSE to skip silently.
*/
public function getSaveToMap() {
return $this->saveToMap;
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Drupal\migrate;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
/**
* Provides the migrate stubbing service.
*/
class MigrateStub implements MigrateStubInterface {
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* Constructs a MigrationStub object.
*
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
*/
public function __construct(MigrationPluginManagerInterface $migration_plugin_manager) {
$this->migrationPluginManager = $migration_plugin_manager;
}
/**
* Creates a stub.
*
* @param string $migration_id
* The migration to stub.
* @param array $source_ids
* An array of source ids.
* @param array $default_values
* (optional) An array of default values to add to the stub.
* @param bool $key_by_destination_ids
* (optional) NULL or TRUE to force indexing of the return array by
* destination id keys (default), or FALSE to return the raw return value of
* the destination plugin's ::import() method. The return value from
* MigrateDestinationInterface::import() is very poorly defined as "The
* entity ID or an indication of success". In practice, the mapping systems
* expect and all destination plugins return an array of destination
* identifiers. Unfortunately these arrays are inconsistently keyed. The
* core destination plugins return a numerically indexed array of
* destination identifiers, but several contrib destinations return an array
* of identifiers indexed by the destination keys. This method will
* generally index all return arrays for consistency and to provide as much
* information as possible, but this parameter is added for backwards
* compatibility to allow accessing the original array.
*
* @return array|false
* An array of destination ids for the new stub, keyed by destination id
* key, or false if the stub failed.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\migrate\MigrateException
*/
public function createStub($migration_id, array $source_ids, array $default_values = [], $key_by_destination_ids = NULL) {
$migrations = $this->migrationPluginManager->createInstances([$migration_id]);
if (!$migrations) {
throw new PluginNotFoundException($migration_id);
}
if (count($migrations) !== 1) {
throw new \LogicException(sprintf('Cannot stub derivable migration "%s". You must specify the id of a specific derivative to stub.', $migration_id));
}
$migration = reset($migrations);
$source_id_keys = array_keys($migration->getSourcePlugin()->getIds());
if (count($source_id_keys) !== count($source_ids)) {
throw new \InvalidArgumentException('Expected and provided source id counts do not match.');
}
if (array_keys($source_ids) === range(0, count($source_ids) - 1)) {
$source_ids = array_combine($source_id_keys, $source_ids);
}
$stub = $this->doCreateStub($migration, $source_ids, $default_values);
// If the return from ::import is numerically indexed, and we aren't
// requesting the raw return value, index it associatively using the
// destination id keys.
if (($key_by_destination_ids !== FALSE) && array_keys($stub) === range(0, count($stub) - 1)) {
$stub = array_combine(array_keys($migration->getDestinationPlugin()->getIds()), $stub);
}
return $stub;
}
/**
* Creates a stub.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to use to create the stub.
* @param array $source_ids
* The source ids to map to the stub.
* @param array $default_values
* (optional) An array of values to include in the stub.
*
* @return array|bool
* An array of destination ids for the stub.
*
* @throws \Drupal\migrate\MigrateException
*/
protected function doCreateStub(MigrationInterface $migration, array $source_ids, array $default_values = []) {
$destination = $migration->getDestinationPlugin(TRUE);
$process = $migration->getProcess();
$id_map = $migration->getIdMap();
$migrate_executable = new MigrateExecutable($migration);
$row = new Row($source_ids + $migration->getSourceConfiguration(), $migration->getSourcePlugin()->getIds(), TRUE);
$migrate_executable->processRow($row, $process);
foreach ($default_values as $key => $value) {
$row->setDestinationProperty($key, $value);
}
$destination_ids = [];
try {
$destination_ids = $destination->import($row);
}
catch (\Exception $e) {
$id_map->saveMessage($row->getSourceIdValues(), $e->getMessage());
}
if ($destination_ids) {
$id_map->saveIdMapping($row, $destination_ids, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
return $destination_ids;
}
return FALSE;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate;
/**
* Provides an interface for the migrate stub service.
*/
interface MigrateStubInterface {
/**
* Creates a stub.
*
* @param string $migration_id
* The migration to stub.
* @param array $source_ids
* An array of source ids.
* @param array $default_values
* (optional) An array of default values to add to the stub.
*
* @return array|false
* An array of destination ids for the new stub, keyed by destination id
* key, or false if the stub failed.
*/
public function createStub($migration_id, array $source_ids, array $default_values = []);
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Drupal\migrate\Plugin\Derivative;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MigrateEntity implements ContainerDeriverInterface {
/**
* List of derivative definitions.
*
* @var array
*/
protected $derivatives = [];
/**
* The entity definitions.
*
* @var \Drupal\Core\Entity\EntityTypeInterface[]
*/
protected $entityDefinitions;
/**
* Constructs a MigrateEntity object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_definitions
* A list of entity definition objects.
*/
public function __construct(array $entity_definitions) {
$this->entityDefinitions = $entity_definitions;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager')->getDefinitions()
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
if (!empty($this->derivatives) && !empty($this->derivatives[$derivative_id])) {
return $this->derivatives[$derivative_id];
}
$this->getDerivativeDefinitions($base_plugin_definition);
return $this->derivatives[$derivative_id];
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityDefinitions as $entity_type => $entity_info) {
$class = is_subclass_of($entity_info->getClass(), 'Drupal\Core\Config\Entity\ConfigEntityInterface') ?
'Drupal\migrate\Plugin\migrate\destination\EntityConfigBase' :
'Drupal\migrate\Plugin\migrate\destination\EntityContentBase';
$this->derivatives[$entity_type] = [
'id' => "entity:$entity_type",
'class' => $class,
'requirements_met' => 1,
'provider' => $entity_info->getProvider(),
];
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Drupal\migrate\Plugin\Derivative;
use Drupal\migrate\Plugin\migrate\destination\EntityContentComplete;
/**
* Deriver for entity_complete:ENTITY_TYPE entity migrations.
*/
class MigrateEntityComplete extends MigrateEntity {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityDefinitions as $entity_type => $entity_info) {
$this->derivatives[$entity_type] = [
'id' => "entity_complete:$entity_type",
'class' => EntityContentComplete::class,
'requirements_met' => 1,
'provider' => $entity_info->getProvider(),
];
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Drupal\migrate\Plugin\Derivative;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MigrateEntityRevision implements ContainerDeriverInterface {
/**
* List of derivative definitions.
*
* @var array
*/
protected $derivatives = [];
/**
* The entity definitions.
*
* @var \Drupal\Core\Entity\EntityTypeInterface[]
*/
protected $entityDefinitions;
/**
* Constructs a MigrateEntity object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_definitions
* A list of entity definition objects.
*/
public function __construct(array $entity_definitions) {
$this->entityDefinitions = $entity_definitions;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager')->getDefinitions()
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
if (!empty($this->derivatives) && !empty($this->derivatives[$derivative_id])) {
return $this->derivatives[$derivative_id];
}
$this->getDerivativeDefinitions($base_plugin_definition);
return $this->derivatives[$derivative_id];
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityDefinitions as $entity_type => $entity_info) {
if ($entity_info->getKey('revision')) {
$this->derivatives[$entity_type] = [
'id' => "entity_revision:$entity_type",
'class' => 'Drupal\migrate\Plugin\migrate\destination\EntityRevision',
'requirements_met' => 1,
'provider' => $entity_info->getProvider(),
];
}
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,137 @@
<?php
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.
*
* @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 AnnotatedClassDiscoveryAutomatedProviders extends AnnotatedClassDiscovery {
/**
* A utility object that can use active autoloaders to find files for classes.
*
* @var \Drupal\Component\ClassFinder\ClassFinderInterface
*/
protected $finder;
/**
* Constructs an AnnotatedClassDiscoveryAutomatedProviders object.
*
* @param string $subdir
* Either the plugin's subdirectory, for example 'Plugin/views/filter', or
* empty string if plugins are located at the top level of the namespace.
* @param \Traversable $root_namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* If $subdir is not an empty string, it will be appended to each namespace.
* @param string $plugin_definition_annotation_name
* The name of the annotation that contains the plugin definition.
* Defaults to 'Drupal\Component\Annotation\Plugin'.
* @param string[] $annotation_namespaces
* Additional namespaces to scan for annotation definitions.
*/
public function __construct($subdir, \Traversable $root_namespaces, $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin', array $annotation_namespaces = []) {
parent::__construct($subdir, $root_namespaces, $plugin_definition_annotation_name, $annotation_namespaces);
$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}
*/
public function getDefinitions() {
$definitions = [];
$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-4 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 \Drupal\Component\Annotation\AnnotationInterface $annotation */
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 parsed again.
$this->fileCache->set($fileinfo->getPathName(), [NULL]);
}
}
}
}
}
}
// Don't let annotation loaders pile up.
AnnotationRegistry::reset();
return $definitions;
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\migrate\Plugin\Discovery;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
/**
* Remove plugin definitions with non-existing 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.
*/
class ProviderFilterDecorator implements DiscoveryInterface {
use DiscoveryTrait;
/**
* The Discovery object being decorated.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**
* A callable for testing if a provider exists.
*
* @var callable
*/
protected $providerExists;
/**
* Constructs an InheritProviderDecorator object.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
* The object implementing DiscoveryInterface that is being decorated.
* @param callable $provider_exists
* A callable, gets passed a provider name, should return TRUE if the
* provider exists and FALSE if not.
*/
public function __construct(DiscoveryInterface $decorated, callable $provider_exists) {
$this->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
* 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);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\migrate\Plugin\Discovery;
use Drupal\Component\Annotation\Doctrine\StaticReflectionParser as BaseStaticReflectionParser;
/**
* Allows getting the reflection parser for the parent class.
*
* @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 StaticReflectionParser extends BaseStaticReflectionParser {
/**
* If the current class extends another, get the parser for the latter.
*
* @param \Drupal\Component\Annotation\Doctrine\StaticReflectionParser $parser
* The current static parser.
* @param $finder
* The class finder. Must implement
* \Drupal\Component\ClassFinder\ClassFinderInterface, but can do so
* implicitly (i.e., implements the interface's methods but not the actual
* interface).
*
* @return static|null
* The static parser for the parent if there's a parent class or NULL.
*/
public static function getParentParser(BaseStaticReflectionParser $parser, $finder) {
// Ensure the class has been parsed before accessing the parentClassName
// property.
$parser->parse();
if ($parser->parentClassName) {
return new static($parser->parentClassName, $finder, $parser->classAnnotationOptimize);
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\migrate\Plugin\Exception;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
/**
* Defines a class for bad plugin definition exceptions.
*/
class BadPluginDefinitionException extends InvalidPluginDefinitionException {
/**
* Constructs a BadPluginDefinitionException.
*
* @param string $plugin_id
* The plugin ID of the mapper.
* @param string $property
* The name of the property that is missing from the plugin.
* @param int $code
* (optional) The exception code. Defaults to 0.
* @param \Exception|null $previous
* The previous throwable used for exception chaining.
*
* @see \Exception
*/
public function __construct($plugin_id, $property, $code = 0, ?\Exception $previous = NULL) {
$message = sprintf('The %s plugin must define the %s property.', $plugin_id, $property);
parent::__construct($plugin_id, $message, $code, $previous);
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\Row;
/**
* Defines an interface for Migration Destination classes.
*
* Destinations are responsible for persisting source data into the destination
* Drupal.
*
* @see \Drupal\migrate\Plugin\migrate\destination\DestinationBase
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Attribute\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*/
interface MigrateDestinationInterface extends PluginInspectionInterface {
/**
* Gets the destination IDs.
*
* To support MigrateIdMap maps, derived destination classes should return
* field definition(s) corresponding to the primary key of the destination
* being implemented. These are used to construct the destination key fields
* of the map table for a migration using this destination.
*
* @return array[]
* An associative array of field definitions keyed by field ID. Values are
* associative arrays with a structure that contains the field type ('type'
* key). The other keys are the field storage settings as they are returned
* by FieldStorageDefinitionInterface::getSettings(). As an example, for a
* composite destination primary key that is defined by an integer and a
* string, the returned value might look like:
* @code
* return [
* 'id' => [
* 'type' => 'integer',
* 'unsigned' => FALSE,
* 'size' => 'big',
* ],
* 'version' => [
* 'type' => 'string',
* 'max_length' => 64,
* 'is_ascii' => TRUE,
* ],
* ];
* @endcode
* If 'type' points to a field plugin with multiple columns and needs to
* refer to a column different than 'value', the key of that column will be
* appended as a suffix to the plugin name, separated by dot ('.'). Example:
* @code
* return [
* 'format' => [
* 'type' => 'text.format',
* ],
* ];
* @endcode
* Additional custom keys/values, that are not part of field storage
* definition, can be passed in definitions:
* @code
* return [
* 'nid' => [
* 'type' => 'integer',
* 'custom_setting' => 'some_value',
* ],
* ];
* @endcode
*
* @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
* @see \Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem
* @see \Drupal\Core\Field\Plugin\Field\FieldType\StringItem
* @see \Drupal\text\Plugin\Field\FieldType\TextItem
*/
public function getIds();
/**
* Returns an array of destination fields.
*
* Derived classes must implement fields(), returning a list of available
* destination fields.
*
* @return array
* - Keys: machine names of the fields
* - Values: Human-friendly descriptions of the fields.
*/
public function fields();
/**
* Import the row.
*
* Derived classes must implement import(), to construct one new object
* (pre-populated) using ID mappings in the Migration.
*
* @param \Drupal\migrate\Row $row
* The row object.
* @param array $old_destination_id_values
* (optional) The destination IDs from the previous import of this source
* row. This is empty the first time a source row is migrated. Defaults to
* an empty array.
*
* @return array|bool
* An indexed array of destination IDs in the same order as defined in the
* plugin's getIds() method if the plugin wants to save the IDs to the ID
* map, TRUE to indicate success without saving IDs to the ID map, or
* FALSE to indicate a failure.
*
* @throws \Drupal\migrate\MigrateException
* Throws an exception if there is a problem importing the row. By default,
* this causes the migration system to treat this row as having failed;
* however, any \Drupal\migrate\Plugin\MigrateIdMapInterface status constant
* can be set using the $status parameter of
* \Drupal\migrate\MigrateException, such as
* \Drupal\migrate\Plugin\MigrateIdMapInterface::STATUS_IGNORED.
*/
public function import(Row $row, array $old_destination_id_values = []);
/**
* Delete the specified destination object from the target Drupal.
*
* @param array $destination_identifier
* The ID of the destination object to delete.
*/
public function rollback(array $destination_identifier);
/**
* Whether the destination can be rolled back or not.
*
* @return bool
* TRUE if rollback is supported, FALSE if not.
*/
public function supportsRollback();
/**
* The rollback action for the last imported item.
*
* @return int
* The MigrateIdMapInterface::ROLLBACK_ constant indicating how an imported
* item should be handled on rollback.
*/
public function rollbackAction();
/**
* Gets the destination module handling the destination data.
*
* @return string|null
* The destination module or NULL if not found.
*/
public function getDestinationModule();
}

View File

@@ -0,0 +1,68 @@
<?php
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\Attribute\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*/
class MigrateDestinationPluginManager extends MigratePluginManager {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a MigrateDestinationPluginManager object.
*
* @param string $type
* The type of the plugin: row, source, process, destination, entity_field,
* id_map.
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* 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, $attribute = MigrateDestination::class, $annotation = 'Drupal\migrate\Annotation\MigrateDestination') {
parent::__construct($type, $namespaces, $cache_backend, $module_handler, $attribute, $annotation);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*
* A specific createInstance method is necessary to pass the migration on.
*/
public function createInstance($plugin_id, array $configuration = [], ?MigrationInterface $migration = NULL) {
if (str_starts_with($plugin_id, 'entity:') && !$this->entityTypeManager->getDefinition(substr($plugin_id, 7), FALSE)) {
$plugin_id = 'null';
}
return parent::createInstance($plugin_id, $configuration, $migration);
}
}

View File

@@ -0,0 +1,311 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Row;
// cspell:ignore destid sourceid
/**
* Defines an interface for migrate ID mappings.
*
* Migrate ID mappings maintain a relation between source ID and destination ID
* for audit and rollback purposes. The keys used in the migrate_map table are
* of the form sourceidN and destidN for the source and destination values
* respectively.
*
* The mappings are stored in a migrate_map table with properties:
* - source_ids_hash: A hash of the source IDs.
* - sourceidN: Any number of source IDs defined by a source plugin, where N
* starts at 1, for example, sourceid1, sourceid2 ... sourceidN.
* - destidN: Any number of destination IDs defined by a destination plugin,
* where N starts at 1, for example, destid1, destid2 ... destidN.
* - source_row_status: Indicates current status of the source row, valid
* values are self::STATUS_IMPORTED, self::STATUS_NEEDS_UPDATE,
* self::STATUS_IGNORED or self::STATUS_FAILED.
* - rollback_action: Flag indicating what to do for this item on rollback. This
* property is set in destination plugins. Valid values are
* self::ROLLBACK_DELETE and self::ROLLBACK_PRESERVE.
* - last_imported: UNIX timestamp of the last time the row was imported.
* - hash: A hash of the source row data that is used to detect changes in the
* source data.
*/
interface MigrateIdMapInterface extends \Iterator, PluginInspectionInterface {
/**
* Indicates that the import of the row was successful.
*/
const STATUS_IMPORTED = 0;
/**
* Indicates that the row needs to be updated.
*/
const STATUS_NEEDS_UPDATE = 1;
/**
* Indicates that the import of the row was ignored.
*/
const STATUS_IGNORED = 2;
/**
* Indicates that the import of the row failed.
*/
const STATUS_FAILED = 3;
/**
* Indicates that the data for the row is to be deleted.
*/
const ROLLBACK_DELETE = 0;
/**
* Indicates that the data for the row is to be preserved.
*
* Rows that refer to entities that already exist on the destination and are
* being updated are preserved.
*/
const ROLLBACK_PRESERVE = 1;
/**
* Saves a mapping from the source identifiers to the destination identifiers.
*
* Called upon import of one row, we record a mapping from the source ID to
* the destination ID. Also may be called, setting the third parameter to
* NEEDS_UPDATE, to signal an existing record should be re-migrated.
*
* @param \Drupal\migrate\Row $row
* The raw source data. We use the ID map derived from the source object
* to get the source identifier values.
* @param array $destination_id_values
* An array of destination identifier values.
* @param int $status
* (optional) Status of the source row in the map. Defaults to
* self::STATUS_IMPORTED.
* @param int $rollback_action
* (optional) How to handle the destination object on rollback. Defaults to
* self::ROLLBACK_DELETE.
*/
public function saveIdMapping(Row $row, array $destination_id_values, $status = self::STATUS_IMPORTED, $rollback_action = self::ROLLBACK_DELETE);
/**
* Saves a message related to a source record in the migration message table.
*
* @param array $source_id_values
* The source identifier keyed values of the record, e.g. ['nid' => 5].
* @param string $message
* The message to record.
* @param int $level
* (optional) The message severity. Defaults to
* MigrationInterface::MESSAGE_ERROR.
*/
public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR);
/**
* Retrieves a traversable object of messages related to source records.
*
* @param array $source_id_values
* (optional) The source identifier keyed values of the record, e.g.
* ['nid' => 5]. If empty (the default), all messages are retrieved.
* @param int $level
* (optional) Message severity. If NULL (the default), retrieve messages of
* all severities.
*
* @return \Traversable
* Retrieves a traversable object of message objects of unspecified class.
* Each object has the following public properties:
* - source_row_hash: the hash of the entire serialized source row data.
* - message: the text of the message.
* - level: one of MigrationInterface::MESSAGE_ERROR,
* MigrationInterface::MESSAGE_WARNING, MigrationInterface::MESSAGE_NOTICE,
* MigrationInterface::MESSAGE_INFORMATIONAL.
*/
public function getMessages(array $source_id_values = [], $level = NULL);
/**
* Prepares to run a full update.
*
* Prepares this migration to run as an update - that is, in addition to
* un-migrated content (source records not in the map table) being imported,
* previously-migrated content will also be updated in place by marking all
* previously-imported content as ready to be re-imported.
*/
public function prepareUpdate();
/**
* Returns the number of processed items in the map.
*
* @return int
* The count of records in the map table.
*/
public function processedCount();
/**
* Returns the number of imported items in the map.
*
* @return int
* The number of imported items.
*/
public function importedCount();
/**
* Returns a count of items which are marked as needing update.
*
* @return int
* The number of items which need updating.
*/
public function updateCount();
/**
* Returns the number of items that failed to import.
*
* @return int
* The number of items that failed to import.
*/
public function errorCount();
/**
* Returns the number of messages saved.
*
* @return int
* The number of messages.
*/
public function messageCount();
/**
* Deletes the map and message entries for a given source record.
*
* @param array $source_id_values
* The source identifier keyed values of the record, e.g. ['nid' => 5].
* @param bool $messages_only
* (optional) TRUE to only delete the migrate messages. Defaults to FALSE.
*/
public function delete(array $source_id_values, $messages_only = FALSE);
/**
* Deletes the map and message table entries for a given destination row.
*
* @param array $destination_id_values
* The destination identifier key value pairs we should do the deletes for.
*/
public function deleteDestination(array $destination_id_values);
/**
* Clears all messages from the map.
*/
public function clearMessages();
/**
* Retrieves a row from the map table based on source identifier values.
*
* @param array $source_id_values
* The source identifier keyed values of the record, e.g. ['nid' => 5].
*
* @return array
* The raw row data as an associative array.
*/
public function getRowBySource(array $source_id_values);
/**
* Retrieves a row by the destination identifiers.
*
* @param array $destination_id_values
* The destination identifier keyed values of the record, e.g. ['nid' => 5].
*
* @return array
* The row(s) of data or an empty array when there is no matching map row.
*/
public function getRowByDestination(array $destination_id_values);
/**
* Retrieves an array of map rows marked as needing update.
*
* @param int $count
* The maximum number of rows to return.
*
* @return array
* Array of map row objects that need updating.
*/
public function getRowsNeedingUpdate($count);
/**
* Looks up the source identifier.
*
* Given a (possibly multi-field) destination identifier value, return the
* (possibly multi-field) source identifier value mapped to it.
*
* @param array $destination_id_values
* The destination identifier keyed values of the record, e.g. ['nid' => 5].
*
* @return array
* The source identifier keyed values of the record, e.g. ['nid' => 5], or
* an empty array on failure.
*/
public function lookupSourceId(array $destination_id_values);
/**
* Looks up the destination identifiers corresponding to a source key.
*
* This can look up a subset of source keys if only some are provided, and
* will return all destination keys that match.
*
* @param array $source_id_values
* The source identifier keyed values of the records, e.g. ['nid' => 5].
* If un-keyed, the first count($source_id_values) keys will be assumed.
*
* @return array
* An array of arrays of destination identifier values.
*
* @throws \Drupal\migrate\MigrateException
* Thrown when $source_id_values contains unknown keys, or is the wrong
* length.
*/
public function lookupDestinationIds(array $source_id_values);
/**
* Looks up the destination identifier currently being iterated.
*
* @return array
* The destination identifier values of the record, or NULL on failure.
*/
public function currentDestination();
/**
* Looks up the source identifier(s) currently being iterated.
*
* @return array
* The source identifier values of the record, or NULL on failure.
*/
public function currentSource();
/**
* Removes any persistent storage used by this map.
*
* For example, remove the map and message tables.
*/
public function destroy();
/**
* Gets the qualified map table.
*
* @todo Remove this as this is SQL only and so doesn't belong to the interface.
*/
public function getQualifiedMapTableName();
/**
* Sets the migrate message service.
*
* @param \Drupal\migrate\MigrateMessageInterface $message
* The migrate message service.
*/
public function setMessage(MigrateMessageInterface $message);
/**
* Sets a specified record to be updated, if it exists.
*
* @param array $source_id_values
* The source identifier values of the record.
*/
public function setUpdate(array $source_id_values);
}

View File

@@ -0,0 +1,75 @@
<?php
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;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages migrate plugins.
*
* @see hook_migrate_info_alter()
* @see \Drupal\migrate\Attribute\MigrateSource
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see \Drupal\migrate\Attribute\MigrateProcess
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
* @see \Drupal\migrate\Plugin\migrate\process\ProcessPluginBase
* @see plugin_api
*
* @ingroup migration
*/
class MigratePluginManager extends DefaultPluginManager implements MigratePluginManagerInterface {
/**
* Constructs a MigratePluginManager object.
*
* @param string $type
* The type of the plugin: row, source, process, destination, entity_field,
* id_map.
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* 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, $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);
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = [], ?MigrationInterface $migration = NULL) {
$plugin_definition = $this->getDefinition($plugin_id);
$plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition);
// If the plugin provides a factory method, pass the container to it.
if (is_subclass_of($plugin_class, 'Drupal\Core\Plugin\ContainerFactoryPluginInterface')) {
$plugin = $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition, $migration);
}
else {
$plugin = new $plugin_class($configuration, $plugin_id, $plugin_definition, $migration);
}
return $plugin;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginManagerInterface;
interface MigratePluginManagerInterface extends PluginManagerInterface {
/**
* Creates a pre-configured instance of a migration plugin.
*
* A specific createInstance method is necessary to pass the migration on.
*
* @param string $plugin_id
* The ID of the plugin being instantiated.
* @param array $configuration
* An array of configuration relevant to the plugin instance.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration context in which the plugin will run.
*
* @return object
* A fully configured plugin instance.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the instance cannot be created, such as if the ID is invalid.
*/
public function createInstance($plugin_id, array $configuration = [], ?MigrationInterface $migration = NULL);
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* An interface for migrate process plugins.
*
* Migrate process plugins transform the input value.For example, transform a
* human provided name into a machine name, look up an identifier in a previous
* migration and so on.
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\ProcessPluginBase
* @see \Drupal\migrate\Attribute\MigrateProcess
* @see plugin_api
*
* @ingroup migration
*/
interface MigrateProcessInterface extends PluginInspectionInterface {
/**
* Performs the associated process.
*
* @param mixed $value
* The value to be transformed.
* @param \Drupal\migrate\MigrateExecutableInterface $migrate_executable
* The migration in which this process is being executed.
* @param \Drupal\migrate\Row $row
* The row from the source to process. Normally, just transforming the value
* is adequate but very rarely you might need to change two columns at the
* same time or something like that.
* @param string $destination_property
* The destination property currently worked on. This is only used together
* with the $row above.
*
* @return mixed
* The newly transformed value.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property);
/**
* Indicates whether the returned value requires multiple handling.
*
* @return bool
* TRUE when the returned value contains a list of values to be processed.
* For example, when the 'source' property is a string and the value found
* is an array.
*/
public function multiple();
/**
* Determines if the pipeline should stop processing.
*
* @return bool
* A boolean value indicating if the pipeline processing should stop.
*/
public function isPipelineStopped(): bool;
/**
* Resets the internal data of a plugin.
*/
public function reset(): void;
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\Row;
/**
* Defines an interface for migrate sources.
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see plugin_api
*
* @ingroup migration
*/
interface MigrateSourceInterface extends \Countable, \Iterator, PluginInspectionInterface {
/**
* Indicates that the source is not countable.
*/
const NOT_COUNTABLE = -1;
/**
* Returns available fields on the source.
*
* @return array
* Available fields in the source, keys are the field machine names as used
* in field mappings, values are descriptions.
*/
public function fields();
/**
* Adds additional data to the row.
*
* @param \Drupal\migrate\Row $row
* The row object.
*
* @return bool
* FALSE if this row needs to be skipped.
*/
public function prepareRow(Row $row);
/**
* Allows class to decide how it will react when it is treated like a string.
*/
public function __toString();
/**
* Defines the source fields uniquely identifying a source row.
*
* None of these fields should contain a NULL value. If necessary, use
* prepareRow() or hook_migrate_prepare_row() to rewrite NULL values to
* appropriate empty values (such as '' or 0).
*
* @return array[]
* An associative array of field definitions keyed by field ID. Values are
* associative arrays with a structure that contains the field type ('type'
* key). The other keys are the field storage settings as they are returned
* by FieldStorageDefinitionInterface::getSettings().
*
* Examples:
*
* A composite source primary key that is defined by an integer and a string
* might look like this:
* @code
* return [
* 'id' => [
* 'type' => 'integer',
* 'unsigned' => FALSE,
* 'size' => 'big',
* ],
* 'version' => [
* 'type' => 'string',
* 'max_length' => 64,
* 'is_ascii' => TRUE,
* ],
* ];
* @endcode
*
* If 'type' points to a field plugin with multiple columns and needs to
* refer to a column different than 'value', the key of that column will be
* appended as a suffix to the plugin name, separated by dot ('.'). Example:
* @code
* return [
* 'format' => [
* 'type' => 'text.format',
* ],
* ];
* @endcode
*
* Additional custom keys/values that are not part of field storage
* definition can be added as shown below. The most common setting
* passed along to the ID definition is table 'alias', used by the SqlBase
* source plugin in order to distinguish between ambiguous column names -
* for example, when a SQL source query joins two tables with the same
* column names.
* @code
* return [
* 'nid' => [
* 'type' => 'integer',
* 'alias' => 'n',
* ],
* ];
* @endcode
*
* @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
* @see \Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem
* @see \Drupal\Core\Field\Plugin\Field\FieldType\StringItem
* @see \Drupal\text\Plugin\Field\FieldType\TextItem
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
*/
public function getIds();
/**
* Gets the source module providing the source data.
*
* @return string|null
* The source module or NULL if not found.
*/
public function getSourceModule();
}

View File

@@ -0,0 +1,73 @@
<?php
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\ProviderFilterDecorator;
/**
* Plugin manager for migrate source plugins.
*
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see \Drupal\migrate\Annotation\MigrateSource
* @see plugin_api
*
* @ingroup migration
*/
class MigrateSourcePluginManager extends MigratePluginManager {
/**
* MigrateSourcePluginManager constructor.
*
* @param string $type
* The type of the plugin: row, source, process, destination, entity_field,
* id_map.
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* 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');
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!$this->discovery) {
$discovery = new AnnotatedClassDiscoveryAutomatedProviders($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
$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);
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* To implement by a destination plugin that should provide entity validation.
*
* @ingroup migration
*/
interface MigrateValidatableEntityInterface {
/**
* Returns a state of whether an entity needs to be validated before saving.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity to check for required validation.
*
* @return bool
* A state of whether an entity needs to be validated.
*/
public function isEntityValidationRequired(FieldableEntityInterface $entity);
/**
* Validates the entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity to validate.
*
* @throws \Drupal\migrate\Exception\EntityValidationException
* When the validation didn't succeed.
*/
public function validateEntity(FieldableEntityInterface $entity);
}

View File

@@ -0,0 +1,796 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\Component\Utility\NestedArray;
use Symfony\Component\DependencyInjection\ContainerInterface;
// cspell:ignore idmap
/**
* Defines the Migration plugin.
*
* A migration plugin instance that represents one single migration and acts
* like a container for the information about a single migration such as the
* source, process and destination plugins.
*
* The configuration of a migration is defined using YAML format and placed in
* the directory MODULENAME/migrations.
*
* Available definition keys:
* - id: The migration ID.
* - label: The human-readable label for the migration.
* - source: The definition for a migrate source plugin.
* - process: The definition for the migrate process pipelines for the
* destination properties.
* - destination: The definition a migrate destination plugin.
* - audit: (optional) Audit the migration for conflicts with existing content.
* - deriver: (optional) The fully qualified path to a deriver class.
* - idMap: (optional) The definition for a migrate idMap plugin.
* - migration_dependencies: (optional) An array with two keys 'required' and
* 'optional' listing the migrations that this migration depends on. The
* required migrations must be run first and completed successfully. The
* optional migrations will be executed if they are present.
* - migration_tags: (optional) An array of tags for this migration.
* - provider: (optional) The name of the module that provides the plugin.
*
* Example with all keys:
*
* @code
* id: d7_taxonomy_term_example
* label: Taxonomy terms
* audit: true
* migration_tags:
* - Drupal 7
* - Content
* - Term example
* deriver: Drupal\taxonomy\Plugin\migrate\D7TaxonomyTermDeriver
* provider: custom_module
* source:
* plugin: d7_taxonomy_term
* process:
* tid: tid
* vid:
* plugin: migration_lookup
* migration: d7_taxonomy_vocabulary
* source: vid
* name: name
* 'description/value': description
* 'description/format': format
* weight: weight
* parent_id:
* -
* plugin: skip_on_empty
* method: process
* source: parent
* -
* plugin: migration_lookup
* migration: d7_taxonomy_term
* parent:
* plugin: default_value
* default_value: 0
* source: '@parent_id'
* destination:
* plugin: entity:taxonomy_term
* migration_dependencies:
* required:
* - d7_taxonomy_vocabulary
* optional:
* - d7_field_instance
* @endcode
*
* For additional configuration keys, refer to these Migrate classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\destination\Config
* @see \Drupal\migrate\Plugin\migrate\destination\EntityConfigBase
* @see \Drupal\migrate\Plugin\migrate\destination\EntityContentBase
* @see \Drupal\Core\Plugin\PluginBase
*
* @link https://www.drupal.org/docs/8/api/migrate-api Migrate API handbook. @endlink
*/
#[\AllowDynamicProperties]
class Migration extends PluginBase implements MigrationInterface, RequirementsInterface, ContainerFactoryPluginInterface {
/**
* The migration ID (machine name).
*
* @var string
*/
protected $id;
/**
* The human-readable label for the migration.
*
* @var string
*/
protected $label;
/**
* The source configuration, with at least a 'plugin' key.
*
* Used to initialize the $sourcePlugin.
*
* @var array
*/
protected $source;
/**
* The source plugin.
*
* @var \Drupal\migrate\Plugin\MigrateSourceInterface
*/
protected $sourcePlugin;
/**
* The configuration describing the process plugins.
*
* This is a strictly internal property and should not returned to calling
* code, use getProcess() instead.
*
* @var array
*/
protected $process = [];
/**
* The cached process plugins.
*
* @var array
*/
protected $processPlugins = [];
/**
* The destination configuration, with at least a 'plugin' key.
*
* Used to initialize $destinationPlugin.
*
* @var array
*/
protected $destination;
/**
* The destination plugin.
*
* @var \Drupal\migrate\Plugin\MigrateDestinationInterface
*/
protected $destinationPlugin;
/**
* The identifier map data.
*
* Used to initialize $idMapPlugin.
*
* @var array
*/
protected $idMap = [];
/**
* The identifier map.
*
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $idMapPlugin;
/**
* The destination identifiers.
*
* An array of destination identifiers: the keys are the name of the
* properties, the values are dependent on the ID map plugin.
*
* @var array
*/
protected $destinationIds = [];
/**
* The source_row_status for the current map row.
*
* @var int
*/
protected $sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
/**
* Track time of last import if TRUE.
*
* @var bool
*
* @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
* replacement.
*
* @see https://www.drupal.org/node/3282894
*/
protected $trackLastImported = FALSE;
/**
* These migrations must be already executed before this migration can run.
*
* @var array
*/
protected $requirements = [];
/**
* An optional list of tags, used by the plugin manager for filtering.
*
* @var array
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
protected $migration_tags = [];
/**
* Whether the migration is auditable.
*
* If set to TRUE, the migration's IDs will be audited. This means that, if
* the highest destination ID is greater than the highest source ID, a warning
* will be displayed that entities might be overwritten.
*
* @var bool
*/
protected $audit = FALSE;
/**
* These migrations, if run, must be executed before this migration.
*
* These are different from the configuration dependencies. Migration
* dependencies are only used to store relationships between migrations.
*
* The migration_dependencies value is structured like this:
* @code
* [
* 'required' => [
* // An array of migration IDs that must be run before this migration.
* ],
* 'optional' => [
* // An array of migration IDs that, if they exist, must be run before
* // this migration.
* ],
* ];
* @endcode
*
* @var array
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
protected $migration_dependencies = [];
/**
* The migration plugin manager for loading other migration plugins.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* The source plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $sourcePluginManager;
/**
* The process plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $processPluginManager;
/**
* The destination plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrateDestinationPluginManager
*/
protected $destinationPluginManager;
/**
* The ID map plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $idMapPluginManager;
/**
* Labels corresponding to each defined status.
*
* @var array
*/
protected $statusLabels = [
self::STATUS_IDLE => 'Idle',
self::STATUS_IMPORTING => 'Importing',
self::STATUS_ROLLING_BACK => 'Rolling back',
self::STATUS_STOPPING => 'Stopping',
self::STATUS_DISABLED => 'Disabled',
];
/**
* Constructs a Migration.
*
* @param array $configuration
* Plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
* @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $source_plugin_manager
* The source migration plugin manager.
* @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $process_plugin_manager
* The process migration plugin manager.
* @param \Drupal\migrate\Plugin\MigrateDestinationPluginManager $destination_plugin_manager
* The destination migration plugin manager.
* @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $id_map_plugin_manager
* The ID map migration plugin manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManagerInterface $source_plugin_manager, MigratePluginManagerInterface $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManagerInterface $id_map_plugin_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migrationPluginManager = $migration_plugin_manager;
$this->sourcePluginManager = $source_plugin_manager;
$this->processPluginManager = $process_plugin_manager;
$this->destinationPluginManager = $destination_plugin_manager;
$this->idMapPluginManager = $id_map_plugin_manager;
foreach (NestedArray::mergeDeepArray([$plugin_definition, $configuration], TRUE) as $key => $value) {
$this->$key = $value;
}
if (isset($plugin_definition['trackLastImported'])) {
@trigger_error("The key 'trackLastImported' is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3282894", E_USER_DEPRECATED);
}
$this->migration_dependencies = ($this->migration_dependencies ?: []) + ['required' => [], 'optional' => []];
if (count($this->migration_dependencies) !== 2 || !is_array($this->migration_dependencies['required']) || !is_array($this->migration_dependencies['optional'])) {
@trigger_error("Invalid migration dependencies for {$this->id()} is deprecated in drupal:10.1.0 and will cause an error in drupal:11.0.0. See https://www.drupal.org/node/3266691", E_USER_DEPRECATED);
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.migration'),
$container->get('plugin.manager.migrate.source'),
$container->get('plugin.manager.migrate.process'),
$container->get('plugin.manager.migrate.destination'),
$container->get('plugin.manager.migrate.id_map')
);
}
/**
* {@inheritdoc}
*/
public function id() {
return $this->pluginId;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->label;
}
/**
* Retrieves the ID map plugin.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The ID map plugin.
*/
public function getIdMapPlugin() {
return $this->idMapPlugin;
}
/**
* {@inheritdoc}
*/
public function getSourcePlugin() {
if (!isset($this->sourcePlugin)) {
$this->sourcePlugin = $this->sourcePluginManager->createInstance($this->source['plugin'], $this->source, $this);
}
return $this->sourcePlugin;
}
/**
* {@inheritdoc}
*/
public function getProcessPlugins(?array $process = NULL) {
if (!isset($process)) {
$process = $this->getProcess();
}
$index = serialize($process);
if (!isset($this->processPlugins[$index])) {
$this->processPlugins[$index] = [];
foreach ($this->getProcessNormalized($process) as $property => $configurations) {
$this->processPlugins[$index][$property] = [];
foreach ($configurations as $configuration) {
if (isset($configuration['source'])) {
$this->processPlugins[$index][$property][] = $this->processPluginManager->createInstance('get', $configuration, $this);
}
// Get is already handled.
if ($configuration['plugin'] != 'get') {
$this->processPlugins[$index][$property][] = $this->processPluginManager->createInstance($configuration['plugin'], $configuration, $this);
}
if (!$this->processPlugins[$index][$property]) {
throw new MigrateException("Invalid process configuration for $property");
}
}
}
}
return $this->processPlugins[$index];
}
/**
* Resolve shorthands into a list of plugin configurations.
*
* @param array $process
* A process configuration array.
*
* @return array
* The normalized process configuration.
*/
protected function getProcessNormalized(array $process) {
$normalized_configurations = [];
foreach ($process as $destination => $configuration) {
if (is_string($configuration)) {
$configuration = [
'plugin' => 'get',
'source' => $configuration,
];
}
if (isset($configuration['plugin'])) {
$configuration = [$configuration];
}
if (!is_array($configuration)) {
$migration_id = $this->getPluginId();
throw new MigrateException("Invalid process for destination '$destination' in migration '$migration_id'");
}
$normalized_configurations[$destination] = $configuration;
}
return $normalized_configurations;
}
/**
* {@inheritdoc}
*/
public function getDestinationPlugin($stub_being_requested = FALSE) {
if ($stub_being_requested && !empty($this->destination['no_stub'])) {
throw new MigrateSkipRowException('Stub requested but not made because no_stub configuration is set.');
}
if (!isset($this->destinationPlugin)) {
$this->destinationPlugin = $this->destinationPluginManager->createInstance($this->destination['plugin'], $this->destination, $this);
}
return $this->destinationPlugin;
}
/**
* {@inheritdoc}
*/
public function getIdMap() {
if (!isset($this->idMapPlugin)) {
$configuration = $this->idMap;
$plugin = $configuration['plugin'] ?? 'sql';
$this->idMapPlugin = $this->idMapPluginManager->createInstance($plugin, $configuration, $this);
}
return $this->idMapPlugin;
}
/**
* {@inheritdoc}
*/
public function getRequirements(): array {
return $this->requirements;
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
// Check whether the current migration source and destination plugin
// requirements are met or not.
if ($this->getSourcePlugin() instanceof RequirementsInterface) {
$this->getSourcePlugin()->checkRequirements();
}
if ($this->getDestinationPlugin() instanceof RequirementsInterface) {
$this->getDestinationPlugin()->checkRequirements();
}
if (empty($this->requirements)) {
// There are no requirements to check.
return;
}
/** @var \Drupal\migrate\Plugin\MigrationInterface[] $required_migrations */
$required_migrations = $this->getMigrationPluginManager()->createInstances($this->requirements);
$missing_migrations = array_diff($this->requirements, array_keys($required_migrations));
// Check if the dependencies are in good shape.
foreach ($required_migrations as $migration_id => $required_migration) {
if (!$required_migration->allRowsProcessed()) {
$missing_migrations[] = $migration_id;
}
}
if ($missing_migrations) {
throw new RequirementsException('Missing migrations ' . implode(', ', $missing_migrations) . '.', ['requirements' => $missing_migrations]);
}
}
/**
* Gets the migration plugin manager.
*
* @return \Drupal\migrate\Plugin\MigrationPluginManagerInterface
* The migration plugin manager.
*/
protected function getMigrationPluginManager() {
return $this->migrationPluginManager;
}
/**
* {@inheritdoc}
*/
public function setStatus($status) {
\Drupal::keyValue('migrate_status')->set($this->id(), $status);
}
/**
* {@inheritdoc}
*/
public function getStatus() {
return \Drupal::keyValue('migrate_status')->get($this->id(), static::STATUS_IDLE);
}
/**
* {@inheritdoc}
*/
public function getStatusLabel() {
$status = $this->getStatus();
if (isset($this->statusLabels[$status])) {
return $this->statusLabels[$status];
}
else {
return '';
}
}
/**
* {@inheritdoc}
*/
public function getInterruptionResult() {
return \Drupal::keyValue('migrate_interruption_result')->get($this->id(), static::RESULT_INCOMPLETE);
}
/**
* {@inheritdoc}
*/
public function clearInterruptionResult() {
\Drupal::keyValue('migrate_interruption_result')->delete($this->id());
}
/**
* {@inheritdoc}
*/
public function interruptMigration($result) {
$this->setStatus(MigrationInterface::STATUS_STOPPING);
\Drupal::keyValue('migrate_interruption_result')->set($this->id(), $result);
}
/**
* {@inheritdoc}
*/
public function allRowsProcessed() {
$source_count = $this->getSourcePlugin()->count();
// If the source is uncountable, we have no way of knowing if it's
// complete, so stipulate that it is.
if ($source_count < 0) {
return TRUE;
}
$processed_count = $this->getIdMap()->processedCount();
// We don't use == because in some circumstances (like unresolved stubs
// being created), the processed count may be higher than the available
// source rows.
return $source_count <= $processed_count;
}
/**
* {@inheritdoc}
*/
public function set($property_name, $value) {
if ($property_name == 'source') {
// Invalidate the source plugin.
unset($this->sourcePlugin);
}
elseif ($property_name === 'destination') {
// Invalidate the destination plugin.
unset($this->destinationPlugin);
}
elseif ($property_name === 'migration_dependencies') {
$value = ($value ?: []) + ['required' => [], 'optional' => []];
if (count($value) !== 2 || !is_array($value['required']) || !is_array($value['optional'])) {
@trigger_error("Invalid migration dependencies for {$this->id()} is deprecated in drupal:10.1.0 and will cause an error in drupal:11.0.0. See https://www.drupal.org/node/3266691", E_USER_DEPRECATED);
}
}
$this->{$property_name} = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function getProcess() {
return $this->getProcessNormalized($this->process);
}
/**
* {@inheritdoc}
*/
public function setProcess(array $process) {
$this->process = $process;
return $this;
}
/**
* {@inheritdoc}
*/
public function setProcessOfProperty($property, $process_of_property) {
$this->process[$property] = $process_of_property;
return $this;
}
/**
* {@inheritdoc}
*/
public function mergeProcessOfProperty($property, array $process_of_property) {
// If we already have a process value then merge the incoming process array
// otherwise simply set it.
$current_process = $this->getProcess();
if (isset($current_process[$property])) {
$this->process = NestedArray::mergeDeepArray([$current_process, $this->getProcessNormalized([$property => $process_of_property])], TRUE);
}
else {
$this->setProcessOfProperty($property, $process_of_property);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function isTrackLastImported() {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3282894', E_USER_DEPRECATED);
return $this->trackLastImported;
}
/**
* {@inheritdoc}
*/
public function setTrackLastImported($track_last_imported) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3282894', E_USER_DEPRECATED);
$this->trackLastImported = (bool) $track_last_imported;
return $this;
}
/**
* Get the dependencies for this migration.
*
* @param bool $expand
* Will issue a deprecation in Drupal 10 if set to FALSE. See
* https://www.drupal.org/node/3266691.
*
* @return array
* The dependencies for this migrations.
*/
public function getMigrationDependencies(bool $expand = FALSE) {
if (!$expand) {
@trigger_error('Calling Migration::getMigrationDependencies() without expanding the plugin IDs is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. In most cases, use getMigrationDependencies(TRUE). See https://www.drupal.org/node/3266691', E_USER_DEPRECATED);
}
// @todo Before Drupal 11.0.0, remove ::set() and these checks.
// @see https://www.drupal.org/project/drupal/issues/3262395
$this->migration_dependencies = ($this->migration_dependencies ?: []) + ['required' => [], 'optional' => []];
if (count($this->migration_dependencies) !== 2 || !is_array($this->migration_dependencies['required']) || !is_array($this->migration_dependencies['optional'])) {
throw new InvalidPluginDefinitionException($this->id(), "Invalid migration dependencies configuration for migration {$this->id()}");
}
$this->migration_dependencies['optional'] = array_unique(array_merge($this->migration_dependencies['optional'], $this->findMigrationDependencies($this->process)));
if (!$expand) {
return $this->migration_dependencies;
}
return array_map(
[$this->migrationPluginManager, 'expandPluginIds'],
$this->migration_dependencies
);
}
/**
* Find migration dependencies from migration_lookup and sub_process plugins.
*
* @param array $process
* A process configuration array.
*
* @return array
* The migration dependencies.
*/
protected function findMigrationDependencies($process) {
$return = [];
foreach ($this->getProcessNormalized($process) as $process_pipeline) {
foreach ($process_pipeline as $plugin_configuration) {
// If the migration uses a deriver and has a migration_lookup with
// itself as the source migration, then skip adding dependencies.
// Otherwise the migration will depend on all the variations of itself.
// See d7_taxonomy_term for an example.
if (isset($this->deriver)
&& $plugin_configuration['plugin'] === 'migration_lookup'
&& $plugin_configuration['migration'] == $this->getBaseId()) {
continue;
}
if (in_array($plugin_configuration['plugin'], ['migration', 'migration_lookup'], TRUE)) {
$return = array_merge($return, (array) $plugin_configuration['migration']);
}
if (in_array($plugin_configuration['plugin'], ['iterator', 'sub_process'], TRUE)) {
$return = array_merge($return, $this->findMigrationDependencies($plugin_configuration['process']));
}
}
}
return $return;
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition() {
$definition = [];
// While normal plugins do not change their definitions on the fly, this
// one does so accommodate for that.
foreach (parent::getPluginDefinition() as $key => $value) {
$definition[$key] = $this->$key ?? $value;
}
return $definition;
}
/**
* {@inheritdoc}
*/
public function getDestinationConfiguration() {
return $this->destination;
}
/**
* {@inheritdoc}
*/
public function getSourceConfiguration() {
return $this->source;
}
/**
* {@inheritdoc}
*/
public function getTrackLastImported() {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3282894', E_USER_DEPRECATED);
return $this->trackLastImported;
}
/**
* {@inheritdoc}
*/
public function getDestinationIds() {
return $this->destinationIds;
}
/**
* {@inheritdoc}
*/
public function getMigrationTags() {
return $this->migration_tags;
}
/**
* {@inheritdoc}
*/
public function isAuditable() {
return (bool) $this->audit;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\migrate\Plugin;
/**
* Provides functionality for migration derivers.
*/
trait MigrationDeriverTrait {
/**
* Returns a fully initialized instance of a source plugin.
*
* @param string $source_plugin_id
* The source plugin ID.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|\Drupal\migrate\Plugin\RequirementsInterface
* The fully initialized source plugin.
*/
public static function getSourcePlugin($source_plugin_id) {
$definition = [
'source' => [
'ignore_map' => TRUE,
'plugin' => $source_plugin_id,
],
'destination' => [
'plugin' => 'null',
],
'idMap' => [
'plugin' => 'null',
],
];
return \Drupal::service('plugin.manager.migration')->createStubMigration($definition)->getSourcePlugin();
}
}

View File

@@ -0,0 +1,350 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Interface for migrations.
*/
interface MigrationInterface extends PluginInspectionInterface, DerivativeInspectionInterface {
/**
* The migration is currently not running.
*/
const STATUS_IDLE = 0;
/**
* The migration is currently importing.
*/
const STATUS_IMPORTING = 1;
/**
* The migration is currently being rolled back.
*/
const STATUS_ROLLING_BACK = 2;
/**
* The migration is being stopped.
*/
const STATUS_STOPPING = 3;
/**
* The migration has been disabled.
*/
const STATUS_DISABLED = 4;
/**
* Migration error.
*/
const MESSAGE_ERROR = 1;
/**
* Migration warning.
*/
const MESSAGE_WARNING = 2;
/**
* Migration notice.
*/
const MESSAGE_NOTICE = 3;
/**
* Migration info.
*/
const MESSAGE_INFORMATIONAL = 4;
/**
* All records have been processed.
*/
const RESULT_COMPLETED = 1;
/**
* The process has stopped itself (e.g., the memory limit is approaching).
*/
const RESULT_INCOMPLETE = 2;
/**
* The process was stopped externally (e.g., via drush migrate-stop).
*/
const RESULT_STOPPED = 3;
/**
* The process had a fatal error.
*/
const RESULT_FAILED = 4;
/**
* Dependencies are unfulfilled - skip the process.
*/
const RESULT_SKIPPED = 5;
/**
* This migration is disabled, skipping.
*/
const RESULT_DISABLED = 6;
/**
* An alias for getPluginId() for backwards compatibility reasons.
*
* @return string
* The plugin_id of the plugin instance.
*
* @see \Drupal\migrate\Plugin\MigrationInterface::getPluginId()
*/
public function id();
/**
* Get the plugin label.
*
* @return string
* The label for this migration.
*/
public function label();
/**
* Get a list of required plugin IDs.
*
* @return string[]
*/
public function getRequirements(): array;
/**
* Returns the initialized source plugin.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface
* The source plugin.
*/
public function getSourcePlugin();
/**
* Returns the process plugins.
*
* @param array $process
* A process configuration array.
*
* @return \Drupal\migrate\Plugin\MigrateProcessInterface[][]
* An associative array. The keys are the destination property names. Values
* are process pipelines. Each pipeline contains an array of plugins.
*/
public function getProcessPlugins(?array $process = NULL);
/**
* Returns the initialized destination plugin.
*
* @param bool $stub_being_requested
* TRUE to indicate that this destination will be asked to construct a stub.
*
* @return \Drupal\migrate\Plugin\MigrateDestinationInterface
* The destination plugin.
*/
public function getDestinationPlugin($stub_being_requested = FALSE);
/**
* Returns the initialized id_map plugin.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The ID map.
*/
public function getIdMap();
/**
* Check if all source rows from this migration have been processed.
*
* @return bool
* TRUE if this migration is complete otherwise FALSE.
*/
public function allRowsProcessed();
/**
* Set the current migration status.
*
* @param int $status
* One of the STATUS_* constants.
*/
public function setStatus($status);
/**
* Get the current migration status.
*
* @return int
* The current migration status. Defaults to STATUS_IDLE.
*/
public function getStatus();
/**
* Retrieve a label for the current status.
*
* @return string
* User-friendly string corresponding to a STATUS_ constant.
*/
public function getStatusLabel();
/**
* Get the result to return upon interruption.
*
* @return int
* The current interruption result. Defaults to RESULT_INCOMPLETE.
*/
public function getInterruptionResult();
/**
* Clears the result to return upon interruption.
*/
public function clearInterruptionResult();
/**
* Sets the migration status as interrupted with a given result code.
*
* @param int $result
* One of the MigrationInterface::RESULT_* constants.
*/
public function interruptMigration($result);
/**
* Gets the normalized process plugin configuration.
*
* The process configuration is always normalized. All shorthand processing
* will be expanded into their full representations.
*
* @see https://www.drupal.org/node/2129651#get-shorthand
*
* @return array
* The normalized configuration describing the process plugins.
*/
public function getProcess();
/**
* Allows you to override the entire process configuration.
*
* @param array $process
* The entire process pipeline configuration describing the process plugins.
*
* @return $this
*/
public function setProcess(array $process);
/**
* Set the process pipeline configuration for an individual destination field.
*
* This method allows you to set the process pipeline configuration for a
* single property within the full process pipeline configuration.
*
* @param string $property
* The property of which to set the process pipeline configuration.
* @param mixed $process_of_property
* The process pipeline configuration to be set for this property.
*
* @return $this
* The migration entity.
*/
public function setProcessOfProperty($property, $process_of_property);
/**
* Merge the process pipeline configuration for a single property.
*
* @param string $property
* The property of which to merge the passed in process pipeline
* configuration.
* @param array $process_of_property
* The process pipeline configuration to be merged with the existing process
* pipeline configuration.
*
* @return $this
* The migration entity.
*/
public function mergeProcessOfProperty($property, array $process_of_property);
/**
* Checks if the migration should track time of last import.
*
* @return bool
* TRUE if the migration is tracking last import time.
*
* @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
* replacement.
*
* @see https://www.drupal.org/node/3282894
*/
public function isTrackLastImported();
/**
* Set if the migration should track time of last import.
*
* @param bool $track_last_imported
* Boolean value to indicate if the migration should track last import time.
*
* @return $this
*
* @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
* replacement.
*
* @see https://www.drupal.org/node/3282894
*/
public function setTrackLastImported($track_last_imported);
/**
* Get the dependencies for this migration.
*
* @return array
* The dependencies for this migrations.
*/
public function getMigrationDependencies();
/**
* Get the destination configuration, with at least a 'plugin' key.
*
* @return array
* The destination configuration.
*/
public function getDestinationConfiguration();
/**
* Get the source configuration, with at least a 'plugin' key.
*
* @return array
* The source configuration.
*/
public function getSourceConfiguration();
/**
* If true, track time of last import.
*
* @return bool
* Flag to determine desire of tracking time of last import.
*
* @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
* replacement.
*
* @see https://www.drupal.org/node/3282894
*/
public function getTrackLastImported();
/**
* The destination identifiers.
*
* An array of destination identifiers: the keys are the name of the
* properties, the values are dependent on the ID map plugin.
*
* @return array
* Destination identifiers.
*/
public function getDestinationIds();
/**
* The migration tags.
*
* @return array
* Migration tags.
*/
public function getMigrationTags();
/**
* Indicates if the migration is auditable.
*
* @return bool
*/
public function isAuditable();
}

View File

@@ -0,0 +1,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(TRUE));
}
// 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(TRUE);
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);
});
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Migration plugin manager interface.
*/
interface MigrationPluginManagerInterface extends PluginManagerInterface {
/**
* Create pre-configured instance of plugin derivatives.
*
* @param array $id
* Either the plugin ID or the base plugin ID of the plugins being
* instantiated. Also accepts an array of plugin IDs and an empty array to
* load all plugins.
* @param array $configuration
* An array of configuration relevant to the plugin instances. Keyed by the
* plugin ID.
*
* @return \Drupal\migrate\Plugin\MigrationInterface[]
* Fully configured plugin instances.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If an instance cannot be created, such as if the ID is invalid.
*/
public function createInstances($id, array $configuration = []);
/**
* Expand derivative migration dependencies.
*
* @param string[] $migration_ids
* A list of plugin IDs.
*
* @return array
* An array of expanded plugin ids.
*/
public function expandPluginIds(array $migration_ids);
/**
* Creates a stub migration plugin from a definition array.
*
* @param array $definition
* The migration definition. If an 'id' key is set then this will be used as
* the migration ID, if not a random ID will be assigned.
*
* @return \Drupal\migrate\Plugin\Migration
* The stub migration.
*/
public function createStubMigration(array $definition);
/**
* Create migrations given a tag.
*
* @param string $tag
* A migration tag we want to filter by.
*
* @return array|\Drupal\migrate\Plugin\MigrationInterface[]
* An array of migration objects with the given tag, or an empty array if no
* migrations with that tag exist.
*/
public function createInstancesByTag($tag);
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
/**
* Remove definitions which refer to a non-existing source plugin.
*/
class NoSourcePluginDecorator implements DiscoveryInterface {
use DiscoveryTrait;
/**
* The Discovery object being decorated.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**
* Constructs a NoSourcePluginDecorator object.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
* The object implementing DiscoveryInterface that is being decorated.
*/
public function __construct(DiscoveryInterface $decorated) {
$this->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);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\migrate\Event\ImportAwareInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\Event\RollbackAwareInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Event subscriber to forward Migrate events to source and destination plugins.
*/
class PluginEventSubscriber implements EventSubscriberInterface {
/**
* Tries to invoke event handling methods on source and destination plugins.
*
* @param string $method
* The method to invoke.
* @param \Drupal\migrate\Event\MigrateImportEvent|\Drupal\migrate\Event\MigrateRollbackEvent $event
* The event that has triggered the invocation.
* @param string $plugin_interface
* The interface which plugins must implement in order to be invoked.
*/
protected function invoke($method, $event, $plugin_interface) {
$migration = $event->getMigration();
$source = $migration->getSourcePlugin();
if ($source instanceof $plugin_interface) {
call_user_func([$source, $method], $event);
}
$destination = $migration->getDestinationPlugin();
if ($destination instanceof $plugin_interface) {
call_user_func([$destination, $method], $event);
}
}
/**
* Forwards pre-import events to the source and destination plugins.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The import event.
*/
public function preImport(MigrateImportEvent $event) {
$this->invoke('preImport', $event, ImportAwareInterface::class);
}
/**
* Forwards post-import events to the source and destination plugins.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The import event.
*/
public function postImport(MigrateImportEvent $event) {
$this->invoke('postImport', $event, ImportAwareInterface::class);
}
/**
* Forwards pre-rollback events to the source and destination plugins.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The rollback event.
*/
public function preRollback(MigrateRollbackEvent $event) {
$this->invoke('preRollback', $event, RollbackAwareInterface::class);
}
/**
* Forwards post-rollback events to the source and destination plugins.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The rollback event.
*/
public function postRollback(MigrateRollbackEvent $event) {
$this->invoke('postRollback', $event, RollbackAwareInterface::class);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = [];
$events[MigrateEvents::PRE_IMPORT][] = ['preImport'];
$events[MigrateEvents::POST_IMPORT][] = ['postImport'];
$events[MigrateEvents::PRE_ROLLBACK][] = ['preRollback'];
$events[MigrateEvents::POST_ROLLBACK][] = ['postRollback'];
return $events;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Drupal\migrate\Plugin;
/**
* An interface to check for a migrate plugin requirements.
*/
interface RequirementsInterface {
/**
* Checks if requirements for this plugin are OK.
*
* @throws \Drupal\migrate\Exception\RequirementsException
* Thrown when requirements are not met.
*/
public function checkRequirements();
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a destination plugin for migrating entity display components.
*
* Display modes provide different presentations for viewing ('view modes') or
* editing ('form modes') content. This destination plugin is an abstract base
* class for migrating fields and other components into view and form modes.
*
* @see \Drupal\migrate\Plugin\migrate\destination\PerComponentEntityDisplay
* @see \Drupal\migrate\Plugin\migrate\destination\PerComponentEntityFormDisplay
*/
abstract class ComponentEntityDisplayBase extends DestinationBase implements ContainerFactoryPluginInterface {
const MODE_NAME = '';
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* PerComponentEntityDisplay constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityDisplayRepositoryInterface $entity_display_repository) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->entityDisplayRepository = $entity_display_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_display.repository')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$values = [];
// array_intersect_key() won't work because the order is important because
// this is also the return value.
foreach (array_keys($this->getIds()) as $id) {
$values[$id] = $row->getDestinationProperty($id);
}
$entity = $this->getEntity($values['entity_type'], $values['bundle'], $values[static::MODE_NAME]);
if (!$row->getDestinationProperty('hidden')) {
$entity->setComponent($values['field_name'], $row->getDestinationProperty('options') ?: []);
}
else {
$entity->removeComponent($values['field_name']);
}
$entity->save();
return array_values($values);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['entity_type']['type'] = 'string';
$ids['bundle']['type'] = 'string';
$ids[static::MODE_NAME]['type'] = 'string';
$ids['field_name']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
// This is intentionally left empty.
}
/**
* Gets the entity.
*
* @param string $entity_type
* The entity type to retrieve.
* @param string $bundle
* The entity bundle.
* @param string $mode
* The display mode.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface
* The entity display object.
*/
abstract protected function getEntity($entity_type, $bundle, $mode);
}

View File

@@ -0,0 +1,245 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides Configuration Management destination plugin.
*
* Persists data to the config system.
*
* Available configuration keys:
* - store null: (optional) Boolean, if TRUE, when a property is NULL, NULL is
* stored, otherwise the default is used. Defaults to FALSE.
* - translations: (optional) Boolean, if TRUE, the destination will be
* associated with the langcode provided by the source plugin. Defaults to
* FALSE.
*
* Destination properties expected in the imported row:
* - config_name: The machine name of the config.
* - langcode: (optional) The language code of the config.
*
* Examples:
*
* @code
* source:
* plugin: variable
* variables:
* - node_admin_theme
* process:
* use_admin_theme: node_admin_theme
* destination:
* plugin: config
* config_name: node.settings
* @endcode
*
* This will add the value of the variable "node_admin_theme" to the config with
* the machine name "node.settings" as "node.settings.use_admin_theme".
*
* @code
* source:
* plugin: d6_variable_translation
* variables:
* - site_offline_message
* process:
* langcode: language
* message: site_offline_message
* destination:
* plugin: config
* config_name: system.maintenance
* translations: true
* @endcode
*
* This will add the value of the variable "site_offline_message" to the config
* with the machine name "system.maintenance" as "system.maintenance.message",
* coupled with the relevant langcode as obtained from the
* "d6_variable_translation" source plugin.
*
* @see \Drupal\migrate_drupal\Plugin\migrate\source\d6\VariableTranslation
*/
#[MigrateDestination('config')]
class Config extends DestinationBase implements ContainerFactoryPluginInterface, DependentPluginInterface {
use DependencyTrait;
/**
* The config object.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
protected $language_manager;
/**
* The typed config manager service.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected TypedConfigManagerInterface $typedConfigManager;
/**
* Constructs a Config destination object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
* The typed config manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, ?TypedConfigManagerInterface $typed_config_manager = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->config = $config_factory->getEditable($configuration['config_name']);
$this->language_manager = $language_manager;
if ($this->isTranslationDestination()) {
$this->supportsRollback = TRUE;
}
if ($typed_config_manager === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $typed_config_manager argument is deprecated in drupal:10.3.0 and is removed in drupal:11.0.0. See https://www.drupal.org/node/3440502', E_USER_DEPRECATED);
$typed_config_manager = \Drupal::service(TypedConfigManagerInterface::class);
}
$this->typedConfigManager = $typed_config_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('config.factory'),
$container->get('language_manager'),
$container->get(TypedConfigManagerInterface::class)
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
if ($this->isTranslationDestination()) {
$this->config = $this->language_manager->getLanguageConfigOverride($row->getDestinationProperty('langcode'), $this->config->getName());
}
foreach ($row->getRawDestination() as $key => $value) {
if (isset($value) || !empty($this->configuration['store null'])) {
$this->config->set(str_replace(Row::PROPERTY_SEPARATOR, '.', $key), $value);
}
}
$name = $this->config->getName();
// Ensure that translatable config has `langcode` specified.
// @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint
if ($this->typedConfigManager->hasConfigSchema($name)
&& $this->typedConfigManager->createFromNameAndData($name, $this->config->getRawData())->hasTranslatableElements()
&& !$this->config->get('langcode')
) {
$this->config->set('langcode', $this->language_manager->getDefaultLanguage()->getId());
}
$this->config->save();
$ids[] = $this->config->getName();
if ($this->isTranslationDestination()) {
$ids[] = $row->getDestinationProperty('langcode');
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
// @todo Dynamically fetch fields using Config Schema API.
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['config_name']['type'] = 'string';
if ($this->isTranslationDestination()) {
$ids['langcode']['type'] = 'string';
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$provider = explode('.', $this->config->getName(), 2)[0];
$this->addDependency('module', $provider);
return $this->dependencies;
}
/**
* Get whether this destination is for translations.
*
* @return bool
* Whether this destination is for translations.
*/
protected function isTranslationDestination() {
return !empty($this->configuration['translations']);
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
if ($this->isTranslationDestination()) {
$language = $destination_identifier['langcode'];
$config = $this->language_manager->getLanguageConfigOverride($language, $this->config->getName());
$config->delete();
}
}
/**
* {@inheritdoc}
*/
public function getDestinationModule() {
if (!empty($this->configuration['destination_module'])) {
return $this->configuration['destination_module'];
}
if (!empty($this->pluginDefinition['destination_module'])) {
return $this->pluginDefinition['destination_module'];
}
// Config translations require the config_translation module so set the
// migration provider to 'config_translation'. The corresponding non
// translated configuration is expected to be handled in a separate
// migration.
if (isset($this->configuration['translations'])) {
return 'config_translation';
}
// Get the module handling this configuration object from the config_name,
// which is of the form <module_name>.<configuration object name>
return !empty($this->configuration['config_name']) ? explode('.', $this->configuration['config_name'], 2)[0] : NULL;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\RequirementsInterface;
// cspell:ignore sourceid
/**
* Base class for migrate destination classes.
*
* Migrate destination plugins perform the import operation of the migration.
* Destination plugins extend this abstract base class. A destination plugin
* must implement at least fields(), getIds() and import() methods. Destination
* plugins can also support rollback operations. For more
* information, refer to \Drupal\migrate\Plugin\MigrateDestinationInterface.
*
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Attribute\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*/
abstract class DestinationBase extends PluginBase implements MigrateDestinationInterface, RequirementsInterface {
/**
* Indicates whether the destination can be rolled back.
*
* @var bool
*/
protected $supportsRollback = FALSE;
/**
* The rollback action to be saved for the last imported item.
*
* @var int
*/
protected $rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
/**
* The migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Constructs an entity destination plugin.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
}
/**
* {@inheritdoc}
*/
public function rollbackAction() {
return $this->rollbackAction;
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
if (empty($this->pluginDefinition['requirements_met'])) {
throw new RequirementsException(sprintf("Destination plugin '%s' did not meet the requirements", $this->pluginId));
}
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
// By default we do nothing.
}
/**
* {@inheritdoc}
*/
public function supportsRollback() {
return $this->supportsRollback;
}
/**
* For a destination item being updated, set the appropriate rollback action.
*
* @param array $id_map
* The map row data for the item.
* @param int $update_action
* The rollback action to take if we are updating an existing item.
*/
protected function setRollbackAction(array $id_map, $update_action = MigrateIdMapInterface::ROLLBACK_PRESERVE) {
// If the entity we're updating was previously migrated by us, preserve the
// existing rollback action.
if (isset($id_map['sourceid1'])) {
$this->rollbackAction = $id_map['rollback_action'];
}
// Otherwise, we're updating an entity which already existed on the
// destination and want to make sure we do not delete it on rollback.
else {
$this->rollbackAction = $update_action;
}
}
/**
* {@inheritdoc}
*/
public function getDestinationModule() {
if (!empty($this->configuration['destination_module'])) {
return $this->configuration['destination_module'];
}
if (!empty($this->pluginDefinition['destination_module'])) {
return $this->pluginDefinition['destination_module'];
}
if (is_string($this->migration->provider)) {
return $this->migration->provider;
}
else {
return reset($this->migration->provider);
}
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\EntityFieldDefinitionTrait;
use Drupal\migrate\Plugin\Derivative\MigrateEntity;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
// cspell:ignore tnid
/**
* Provides a generic destination to import entities.
*
* Available configuration keys:
* - default_bundle: (optional) The bundle to use for this row if 'bundle' is
* not defined on the row.
*
* Examples:
*
* @code
* source:
* plugin: d7_node
* process:
* nid: tnid
* vid: vid
* langcode: language
* title: title
* ...
* revision_timestamp: timestamp
* destination:
* plugin: entity:node
* @endcode
*
* This will save the processed, migrated row as a node.
*
* @code
* source:
* plugin: d7_node
* process:
* nid: tnid
* vid: vid
* langcode: language
* title: title
* ...
* revision_timestamp: timestamp
* destination:
* plugin: entity:node
* default_bundle: custom
* @endcode
*
* This will save the processed, migrated row as a node of type 'custom'.
*/
#[MigrateDestination(
id: 'entity',
deriver: MigrateEntity::class
)]
abstract class Entity extends DestinationBase implements ContainerFactoryPluginInterface, DependentPluginInterface {
use DependencyTrait;
use EntityFieldDefinitionTrait;
/**
* The entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* The entity field manager.
*/
protected EntityFieldManagerInterface $entityFieldManager;
/**
* The list of the bundles of this entity type.
*
* @var array
*/
protected $bundles;
/**
* Construct a new entity.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles) {
$plugin_definition += [
'label' => $storage->getEntityType()->getPluralLabel(),
];
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->storage = $storage;
$this->bundles = $bundles;
$this->supportsRollback = TRUE;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type_id = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager')->getStorage($entity_type_id),
array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id))
);
}
/**
* Gets the bundle for the row taking into account the default.
*
* @param \Drupal\migrate\Row $row
* The current row we're importing.
*
* @return string
* The bundle for this row.
*/
public function getBundle(Row $row) {
$default_bundle = $this->configuration['default_bundle'] ?? '';
$bundle_key = $this->getKey('bundle');
return $row->getDestinationProperty($bundle_key) ?: $default_bundle;
}
/**
* {@inheritdoc}
*/
public function fields() {
// @todo Implement fields() method.
}
/**
* Creates or loads an entity.
*
* @param \Drupal\migrate\Row $row
* The row object.
* @param array $old_destination_id_values
* The old destination IDs.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity we are importing into.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
$entity_id = reset($old_destination_id_values) ?: $this->getEntityId($row);
if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
// Allow updateEntity() to change the entity.
$entity = $this->updateEntity($entity, $row) ?: $entity;
}
else {
// Attempt to ensure we always have a bundle.
if ($bundle = $this->getBundle($row)) {
$row->setDestinationProperty($this->getKey('bundle'), $bundle);
}
// Stubs might need some required fields filled in.
if ($row->isStub()) {
$this->processStubRow($row);
}
$entity = $this->storage->create($row->getDestination());
$entity->enforceIsNew();
}
return $entity;
}
/**
* Updates an entity with the new values from row.
*
* This method should be implemented in extending classes.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to update.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*
* @return \Drupal\Core\Entity\EntityInterface
* An updated entity from row values.
*
* @throws \LogicException
* Thrown for config entities, if the destination is for translations and
* either the "property" or "translation" property does not exist.
*/
abstract protected function updateEntity(EntityInterface $entity, Row $row);
/**
* Populates as much of the stub row as possible.
*
* This method can be implemented in extending classes when needed.
*
* @param \Drupal\migrate\Row $row
* The row of data.
*/
protected function processStubRow(Row $row) {}
/**
* Gets the entity ID of the row.
*
* @param \Drupal\migrate\Row $row
* The row of data.
*
* @return string
* The entity ID for the row that we are importing.
*/
protected function getEntityId(Row $row) {
return $row->getDestinationProperty($this->getKey('id'));
}
/**
* Returns a specific entity key.
*
* @param string $key
* The name of the entity key to return.
*
* @return string|bool
* The entity key, or FALSE if it does not exist.
*
* @see \Drupal\Core\Entity\EntityTypeInterface::getKeys()
*/
protected function getKey($key) {
return $this->storage->getEntityType()->getKey($key);
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
// Delete the specified entity from Drupal if it exists.
$entity = $this->storage->load(reset($destination_identifier));
if ($entity) {
if ($entity instanceof ContentEntityInterface) {
$entity->setSyncing(TRUE);
}
$entity->delete();
}
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$this->addDependency('module', $this->storage->getEntityType()->getProvider());
return $this->dependencies;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Row;
/**
* Provides entity base field override destination plugin.
*
* Base fields are non-configurable fields that always exist on a given entity
* type, like the 'title', 'created' and 'sticky' fields of the 'node' entity
* type. Some entity types can have bundles, for example the node content types.
* The base fields exist on all bundles but the bundles can override the
* definitions. For example, the label for node 'title' base field can be
* different on different content types.
*
* Example:
*
* The example below migrates the node 'sticky' settings for each content type.
* @code
* id: d6_node_setting_sticky
* label: Node type 'sticky' setting
* migration_tags:
* - Drupal 6
* source:
* plugin: d6_node_type
* constants:
* entity_type: node
* field_name: sticky
* process:
* entity_type: 'constants/entity_type'
* bundle: type
* field_name: 'constants/field_name'
* label:
* plugin: default_value
* default_value: 'Sticky at the top of lists'
* 'default_value/0/value': 'options/sticky'
* destination:
* plugin: entity:base_field_override
* migration_dependencies:
* required:
* - d6_node_type
* @endcode
*/
#[MigrateDestination('entity:base_field_override')]
class EntityBaseFieldOverride extends EntityConfigBase {
/**
* {@inheritdoc}
*/
protected function getEntityId(Row $row) {
$entity_type = $row->getDestinationProperty('entity_type');
$bundle = $row->getDestinationProperty('bundle');
$field_name = $row->getDestinationProperty('field_name');
return "$entity_type.$bundle.$field_name";
}
}

View File

@@ -0,0 +1,307 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\language\ConfigurableLanguageManager;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base destination class for importing configuration entities.
*
* Available configuration keys:
* - translations: (optional) Boolean, if TRUE, the destination will be
* associated with the langcode provided by the source plugin. Defaults to
* FALSE.
*
* Examples:
*
* @code
* source:
* plugin: d7_block_custom
* process:
* id: bid
* info: info
* langcode: language
* body: body
* destination:
* plugin: entity:block
* @endcode
*
* This will save the migrated, processed row as a block config entity.
*
* @code
* source:
* plugin: d6_profile_field_translation
* constants:
* entity_type: user
* bundle: user
* process:
* langcode: language
* entity_type: 'constants/entity_type'
* bundle: 'constants/bundle'
* field_name: name
* ...
* property: property
* translation: translation
* destination:
* plugin: entity:field_config
* translations: true
* @endcode
*
* Because the translations configuration is set to "true", this will save the
* migrated, processed row to a "field_config" entity associated with the
* designated langcode. Note that the this makes the "translation" and
* "property" properties required.
*/
class EntityConfigBase extends Entity {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Construct a new entity.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles);
$this->languageManager = $language_manager;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type_id = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager')->getStorage($entity_type_id),
array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id)),
$container->get('language_manager'),
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
if ($row->isStub()) {
throw new MigrateException('Config entities can not be stubbed.');
}
$this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
$ids = $this->getIds();
$id_key = $this->getKey('id');
if (count($ids) > 1) {
// Ids is keyed by the key name so grab the keys.
$id_keys = array_keys($ids);
if (!$row->getDestinationProperty($id_key)) {
// Set the ID into the destination in for form "val1.val2.val3".
$row->setDestinationProperty($id_key, $this->generateId($row, $id_keys));
}
}
$entity = $this->getEntity($row, $old_destination_id_values);
// Translations are already saved in updateEntity by configuration override.
if (!$this->isTranslationDestination()) {
$entity->save();
}
if (count($ids) > 1) {
// This can only be a config entity, content entities have their ID key
// and that's it.
$return = [];
foreach ($id_keys as $id_key) {
if (($this->isTranslationDestination()) && ($id_key == 'langcode')) {
// Config entities do not have a language property, get the language
// code from the destination.
$return[] = $row->getDestinationProperty($id_key);
}
else {
$return[] = $entity->get($id_key);
}
}
return $return;
}
return [$entity->id()];
}
/**
* Get whether this destination is for translations.
*
* @return bool
* Whether this destination is for translations.
*/
protected function isTranslationDestination() {
return !empty($this->configuration['translations']);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$id_key = $this->getKey('id');
$ids[$id_key]['type'] = 'string';
if ($this->isTranslationDestination()) {
$ids['langcode']['type'] = 'string';
}
return $ids;
}
/**
* Updates an entity with the contents of a row.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to update.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*
* @return \Drupal\Core\Entity\EntityInterface
* An updated entity from row values.
*
* @throws \LogicException
* Thrown if the destination is for translations and either the "property"
* or "translation" property does not exist.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
// This is a translation if the language in the active config does not
// match the language of this row.
$translation = FALSE;
if ($this->isTranslationDestination() && $row->hasDestinationProperty('langcode') && $this->languageManager instanceof ConfigurableLanguageManager) {
$config = $entity->getConfigDependencyName();
$langcode = $this->configFactory->get('langcode');
if ($langcode != $row->getDestinationProperty('langcode')) {
$translation = TRUE;
}
}
if ($translation) {
if (!$row->hasDestinationProperty('property')) {
throw new \LogicException('The "property" property is required');
}
if (!$row->hasDestinationProperty('translation')) {
throw new \LogicException('The "translation" property is required');
}
$config_override = $this->languageManager->getLanguageConfigOverride($row->getDestinationProperty('langcode'), $config);
$config_override->set(str_replace(Row::PROPERTY_SEPARATOR, '.', $row->getDestinationProperty('property')), $row->getDestinationProperty('translation'));
$config_override->save();
}
else {
foreach ($row->getRawDestination() as $property => $value) {
$this->updateEntityProperty($entity, explode(Row::PROPERTY_SEPARATOR, $property), $value);
}
$this->setRollbackAction($row->getIdMap());
}
return $entity;
}
/**
* Updates a (possible nested) entity property with a value.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The config entity.
* @param array $parents
* The array of parents.
* @param string|object $value
* The value to update to.
*/
protected function updateEntityProperty(EntityInterface $entity, array $parents, $value) {
$top_key = array_shift($parents);
$entity_value = $entity->get($top_key);
if (is_array($entity_value)) {
NestedArray::setValue($entity_value, $parents, $value);
}
else {
$entity_value = $value;
}
$entity->set($top_key, $entity_value);
}
/**
* Generates an entity ID.
*
* @param \Drupal\migrate\Row $row
* The current row.
* @param array $ids
* The destination IDs.
*
* @return string
* The generated entity ID.
*/
protected function generateId(Row $row, array $ids) {
$id_values = [];
foreach ($ids as $id) {
if ($this->isTranslationDestination() && $id == 'langcode') {
continue;
}
$id_values[] = $row->getDestinationProperty($id);
}
return implode('.', $id_values);
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
if ($this->isTranslationDestination()) {
// The entity id does not include the langcode.
$id_values = [];
foreach ($destination_identifier as $key => $value) {
if ($this->isTranslationDestination() && $key === 'langcode') {
continue;
}
$id_values[] = $value;
}
$entity_id = implode('.', $id_values);
$language = $destination_identifier['langcode'];
$config = $this->storage->load($entity_id)->getConfigDependencyName();
$config_override = $this->languageManager->getLanguageConfigOverride($language, $config);
// Rollback the translation.
$config_override->delete();
}
else {
$destination_identifier = implode('.', $destination_identifier);
parent::rollback([$destination_identifier]);
}
}
}

View File

@@ -0,0 +1,398 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\migrate\Audit\HighestIdInterface;
use Drupal\migrate\Exception\EntityValidationException;
use Drupal\migrate\Plugin\MigrateValidatableEntityInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
// cspell:ignore huhuu maailma otsikko sivun validatable
/**
* Provides destination class for all content entities lacking a specific class.
*
* Available configuration keys:
* - translations: (optional) Boolean, indicates if the entity is translatable,
* defaults to FALSE.
* - overwrite_properties: (optional) A list of properties that will be
* overwritten if an entity with the same ID already exists. Any properties
* that are not listed will not be overwritten.
* - validate: (optional) Boolean, indicates whether an entity should be
* validated, defaults to FALSE.
*
* Example:
*
* The example below will create a 'node' entity of content type 'article'.
*
* The language of the source will be used because the configuration
* 'translations: true' was set. Without this configuration option the site's
* default language would be used.
*
* The example content type has fields 'title', 'body' and 'field_example'.
* The text format of the body field is defaulted to 'basic_html'. The example
* uses the EmbeddedDataSource source plugin for the sake of simplicity.
*
* If the migration is executed again in an update mode, any updates done in the
* destination Drupal site to the 'title' and 'body' fields would be overwritten
* with the original source values. Updates done to 'field_example' would be
* preserved because 'field_example' is not included in 'overwrite_properties'
* configuration.
* @code
* id: custom_article_migration
* label: Custom article migration
* source:
* plugin: embedded_data
* data_rows:
* -
* id: 1
* langcode: 'fi'
* title: 'Sivun otsikko'
* field_example: 'Huhuu'
* content: '<p>Hoi maailma</p>'
* ids:
* id:
* type: integer
* process:
* nid: id
* langcode: langcode
* title: title
* field_example: field_example
* 'body/0/value': content
* 'body/0/format':
* plugin: default_value
* default_value: basic_html
* destination:
* plugin: entity:node
* default_bundle: article
* translations: true
* overwrite_properties:
* - title
* - body
* # Run entity and fields validation before saving an entity.
* # @see \Drupal\Core\Entity\FieldableEntityInterface::validate()
* validate: true
* @endcode
*
* @see \Drupal\migrate\Plugin\migrate\destination\Entity
* @see \Drupal\migrate\Plugin\migrate\destination\EntityRevision
*/
class EntityContentBase extends Entity implements HighestIdInterface, MigrateValidatableEntityInterface {
/**
* Field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* The account switcher service.
*
* @var \Drupal\Core\Session\AccountSwitcherInterface
*/
protected $accountSwitcher;
/**
* Constructs a content entity.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
* @param \Drupal\Core\Session\AccountSwitcherInterface $account_switcher
* The account switcher service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ?AccountSwitcherInterface $account_switcher = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles);
$this->entityFieldManager = $entity_field_manager;
$this->fieldTypeManager = $field_type_manager;
$this->accountSwitcher = $account_switcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager')->getStorage($entity_type),
array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type)),
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('account_switcher')
);
}
/**
* {@inheritdoc}
*
* @throws \Drupal\migrate\MigrateException
* When an entity cannot be looked up.
* @throws \Drupal\migrate\Exception\EntityValidationException
* When an entity validation hasn't been passed.
*/
public function import(Row $row, array $old_destination_id_values = []) {
$this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
$entity = $this->getEntity($row, $old_destination_id_values);
if (!$entity) {
throw new MigrateException('Unable to get entity');
}
assert($entity instanceof ContentEntityInterface);
if ($this->isEntityValidationRequired($entity)) {
$this->validateEntity($entity);
}
$ids = $this->save($entity, $old_destination_id_values);
if ($this->isTranslationDestination()) {
$ids[] = $entity->language()->getId();
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function isEntityValidationRequired(FieldableEntityInterface $entity) {
// Prioritize the entity method over migration config because it won't be
// possible to save that entity non validated.
/* @see \Drupal\Core\Entity\ContentEntityBase::preSave() */
return $entity->isValidationRequired() || !empty($this->configuration['validate']);
}
/**
* {@inheritdoc}
*/
public function validateEntity(FieldableEntityInterface $entity) {
// Entity validation can require the user that owns the entity. Switch to
// use that user during validation.
// As an example:
// @see \Drupal\Core\Entity\Plugin\Validation\Constraint\ValidReferenceConstraint
$account = $entity instanceof EntityOwnerInterface ? $entity->getOwner() : NULL;
// Validate account exists as the owner reference could be invalid for any
// number of reasons.
if ($account) {
$this->accountSwitcher->switchTo($account);
}
// This finally block ensures that the account is always switched back, even
// if an exception was thrown. Any validation exceptions are intentionally
// left unhandled. They should be caught and logged by a catch block
// surrounding the row import and then added to the migration messages table
// for the current row.
try {
$violations = $entity->validate();
} finally {
if ($account) {
$this->accountSwitcher->switchBack();
}
}
if (count($violations) > 0) {
throw new EntityValidationException($violations);
}
}
/**
* Saves the entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The content entity.
* @param array $old_destination_id_values
* (optional) An array of destination ID values. Defaults to an empty array.
*
* @return array
* An array containing the entity ID.
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = []) {
$entity->setSyncing(TRUE);
$entity->save();
return [$entity->id()];
}
/**
* {@inheritdoc}
*/
public function isTranslationDestination() {
return !empty($this->configuration['translations']);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids = [];
$id_key = $this->getKey('id');
$ids[$id_key] = $this->getDefinitionFromEntity($id_key);
if ($this->isTranslationDestination()) {
$langcode_key = $this->getKey('langcode');
if (!$langcode_key) {
throw new MigrateException(sprintf('The "%s" entity type does not support translations.', $this->storage->getEntityTypeId()));
}
$ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key);
}
return $ids;
}
/**
* {@inheritdoc}
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
$empty_destinations = $row->getEmptyDestinationProperties();
// By default, an update will be preserved.
$rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE;
// Make sure we have the right translation.
if ($this->isTranslationDestination()) {
$property = $this->storage->getEntityType()->getKey('langcode');
if ($row->hasDestinationProperty($property)) {
$language = $row->getDestinationProperty($property);
if (!$entity->hasTranslation($language)) {
$entity->addTranslation($language);
// We're adding a translation, so delete it on rollback.
$rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE;
}
$entity = $entity->getTranslation($language);
}
}
// If the migration has specified a list of properties to be overwritten,
// clone the row with an empty set of destination values, and re-add only
// the specified properties.
if (isset($this->configuration['overwrite_properties'])) {
$empty_destinations = array_intersect($empty_destinations, $this->configuration['overwrite_properties']);
$clone = $row->cloneWithoutDestination();
foreach ($this->configuration['overwrite_properties'] as $property) {
$clone->setDestinationProperty($property, $row->getDestinationProperty($property));
}
$row = $clone;
}
foreach ($row->getDestination() as $field_name => $values) {
$field = $entity->$field_name;
if ($field instanceof TypedDataInterface) {
$field->setValue($values);
}
}
foreach ($empty_destinations as $field_name) {
$entity->$field_name = NULL;
}
$this->setRollbackAction($row->getIdMap(), $rollback_action);
// We might have a different (translated) entity, so return it.
return $entity;
}
/**
* {@inheritdoc}
*/
protected function processStubRow(Row $row) {
$bundle_key = $this->getKey('bundle');
if ($bundle_key && empty($row->getDestinationProperty($bundle_key))) {
if (empty($this->bundles)) {
throw new MigrateException('Stubbing failed, no bundles available for entity type: ' . $this->storage->getEntityTypeId());
}
$row->setDestinationProperty($bundle_key, reset($this->bundles));
}
$bundle = $row->getDestinationProperty($bundle_key) ?? $this->storage->getEntityTypeId();
// Populate any required fields not already populated.
$fields = $this->entityFieldManager
->getFieldDefinitions($this->storage->getEntityTypeId(), $bundle);
foreach ($fields as $field_name => $field_definition) {
if ($field_definition->isRequired() && is_null($row->getDestinationProperty($field_name))) {
// Use the configured default value for this specific field, if any.
if ($default_value = $field_definition->getDefaultValueLiteral()) {
$values = $default_value;
}
else {
/** @var \Drupal\Core\Field\FieldItemInterface $field_type_class */
$field_type_class = $this->fieldTypeManager
->getPluginClass($field_definition->getType());
$values = $field_type_class::generateSampleValue($field_definition);
if (is_null($values)) {
// Handle failure to generate a sample value.
throw new MigrateException('Stubbing failed, unable to generate value for field ' . $field_name);
}
}
$row->setDestinationProperty($field_name, $values);
}
}
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
if ($this->isTranslationDestination()) {
// Attempt to remove the translation.
$entity = $this->storage->load(reset($destination_identifier));
if ($entity && $entity instanceof TranslatableInterface) {
if ($key = $this->getKey('langcode')) {
if (isset($destination_identifier[$key])) {
$langcode = $destination_identifier[$key];
if ($entity->hasTranslation($langcode)) {
// Make sure we don't remove the default translation.
$translation = $entity->getTranslation($langcode);
if (!$translation->isDefaultTranslation()) {
$entity->removeTranslation($langcode);
$entity->setSyncing(TRUE);
$entity->save();
}
}
}
}
}
}
else {
parent::rollback($destination_identifier);
}
}
/**
* {@inheritdoc}
*/
public function getHighestId() {
$values = $this->storage->getQuery()
->accessCheck(FALSE)
->sort($this->getKey('id'), 'DESC')
->range(0, 1)
->execute();
return (int) current($values);
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\EntityFieldDefinitionTrait;
use Drupal\migrate\Plugin\Derivative\MigrateEntityComplete;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
/**
* Provides a destination for migrating the entire entity revision table.
*/
#[MigrateDestination(
id: 'entity_complete',
deriver: MigrateEntityComplete::class
)]
class EntityContentComplete extends EntityContentBase {
use EntityFieldDefinitionTrait;
/**
* {@inheritdoc}
*/
public function getIds() {
$ids = [];
$id_key = $this->getKey('id');
$ids[$id_key] = $this->getDefinitionFromEntity($id_key);
$revision_key = $this->getKey('revision');
if ($revision_key) {
$ids[$revision_key] = $this->getDefinitionFromEntity($revision_key);
}
$langcode_key = $this->getKey('langcode');
if ($langcode_key) {
$ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key);
}
return $ids;
}
/**
* Gets the entity.
*
* @param \Drupal\migrate\Row $row
* The row object.
* @param array $old_destination_id_values
* The old destination IDs.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
$revision_id = $old_destination_id_values
? $old_destination_id_values[1]
: $row->getDestinationProperty($this->getKey('revision'));
// If we are re-running a migration with set revision IDs and the
// destination revision ID already exists then do not create a new revision.
$entity = NULL;
if (!empty($revision_id)) {
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = $this->storage;
if ($entity = $storage->loadRevision($revision_id)) {
$entity->setNewRevision(FALSE);
}
}
if ($entity === NULL && ($entity_id = $row->getDestinationProperty($this->getKey('id'))) && ($entity = $this->storage->load($entity_id))) {
// We want to create a new entity. Set enforceIsNew() FALSE is necessary
// to properly save a new entity while setting the ID. Without it, the
// system would see that the ID is already set and assume it is an update.
$entity->enforceIsNew(FALSE);
// Intentionally create a new revision. Setting new revision TRUE here may
// not be necessary, it is done for clarity.
$entity->setNewRevision(TRUE);
}
if ($entity === NULL) {
// Attempt to set the bundle.
if ($bundle = $this->getBundle($row)) {
$row->setDestinationProperty($this->getKey('bundle'), $bundle);
}
// Stubs might need some required fields filled in.
if ($row->isStub()) {
$this->processStubRow($row);
}
$entity = $this->storage->create($row->getDestination());
$entity->enforceIsNew();
}
// We need to update the entity, so that the destination row IDs are
// correct.
$entity = $this->updateEntity($entity, $row);
$entity->isDefaultRevision(TRUE);
if ($entity instanceof EntityChangedInterface && $entity instanceof ContentEntityInterface) {
// If we updated any untranslatable fields, update the timestamp for the
// other translations.
/** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\EntityChangedInterface $entity */
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
// If we updated an untranslated field, then set the changed time for
// for all translations to match the current row that we are saving.
// In this context, getChangedTime() should return the value we just
// set in the updateEntity() call above.
if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
$entity->getTranslation($langcode)->setChangedTime($entity->getChangedTime());
}
}
}
return $entity;
}
/**
* {@inheritdoc}
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
$entity = parent::updateEntity($entity, $row);
// Always set the rollback action to delete. This is because the parent
// updateEntity will set the rollback action to preserve for the original
// language row, which is needed for the classic node migrations.
$this->setRollbackAction($row->getIdMap(), MigrateIdMapInterface::ROLLBACK_DELETE);
return $entity;
}
/**
* {@inheritdoc}
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = []) {
parent::save($entity, $old_destination_id_values);
return [
$entity->id(),
$entity->getRevisionId(),
];
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
// We want to delete the entity and all the translations so use
// Entity:rollback because EntityContentBase::rollback will not remove the
// default translation.
Entity::rollback($destination_identifier);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
/**
* Provides destination plugin for field_config configuration entities.
*
* The Field API defines two primary data structures, FieldStorage and Field.
* A FieldStorage defines a particular type of data that can be attached to
* entities as a Field instance.
*
* The example below adds an instance of 'field_text_example' to 'article'
* bundle (node content type). The example uses the EmptySource source plugin
* and constant source values for the sake of simplicity. For an example on how
* the FieldStorage 'field_text_example' can be migrated, refer to
* \Drupal\migrate\Plugin\migrate\destination\EntityFieldStorageConfig.
* @code
* id: field_instance_example
* label: Field instance example
* source:
* plugin: empty
* constants:
* entity_type: node
* field_name: field_text_example
* bundle: article
* label: Text field example
* translatable: true
* process:
* entity_type: constants/entity_type
* field_name: constants/field_name
* bundle: constants/bundle
* label: constants/label
* translatable: constants/translatable
* destination:
* plugin: entity:field_config
* migration_dependencies:
* required:
* - field_storage_example
* @endcode
*
* @see \Drupal\field\Entity\FieldConfig
* @see \Drupal\field\Entity\FieldConfigBase
*/
#[MigrateDestination('entity:field_config')]
class EntityFieldInstance extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['entity_type']['type'] = 'string';
$ids['bundle']['type'] = 'string';
$ids['field_name']['type'] = 'string';
if ($this->isTranslationDestination()) {
$ids['langcode']['type'] = 'string';
}
return $ids;
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
/**
* Provides destination plugin for field_storage_config configuration entities.
*
* The Field API defines two primary data structures, FieldStorage and Field.
* A FieldStorage defines a particular type of data that can be attached to
* entities as a Field instance.
*
* The example below creates a storage for a simple text field. The example uses
* the EmptySource source plugin and constant source values for the sake of
* simplicity.
* @code
* id: field_storage_example
* label: Field storage example
* source:
* plugin: empty
* constants:
* entity_type: node
* id: node.field_text_example
* field_name: field_text_example
* type: string
* cardinality: 1
* settings:
* max_length: 10
* langcode: en
* translatable: true
* process:
* entity_type: constants/entity_type
* id: constants/id
* field_name: constants/field_name
* type: constants/type
* cardinality: constants/cardinality
* settings: constants/settings
* langcode: constants/langcode
* translatable: constants/translatable
* destination:
* plugin: entity:field_storage_config
* @endcode
*
* For a full list of the properties of a FieldStorage configuration entity,
* refer to \Drupal\field\Entity\FieldStorageConfig.
*
* For an example on how to migrate a Field instance of this FieldStorage,
* refer to \Drupal\migrate\Plugin\migrate\destination\EntityFieldInstance.
*/
#[MigrateDestination('entity:field_storage_config')]
class EntityFieldStorageConfig extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['entity_type']['type'] = 'string';
$ids['field_name']['type'] = 'string';
// @todo Remove conditional. https://www.drupal.org/node/3004574
if ($this->isTranslationDestination()) {
$ids['langcode']['type'] = 'string';
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
if ($this->isTranslationDestination()) {
$language = $destination_identifier['langcode'];
unset($destination_identifier['langcode']);
$destination_identifier = [
implode('.', $destination_identifier),
'langcode' => $language,
];
}
else {
$destination_identifier = [implode('.', $destination_identifier)];
}
parent::rollback($destination_identifier);
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\Derivative\MigrateEntityRevision;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Provides entity revision destination plugin.
*
* Refer to the parent class for configuration keys:
* \Drupal\migrate\Plugin\migrate\destination\EntityContentBase
*
* Entity revisions can only be migrated after the entity to which the revisions
* belong has been migrated. For example, revisions of a given content type can
* be migrated only after the nodes of that content type have been migrated.
*
* In order to avoid revision ID conflicts, make sure that the entity migration
* also includes the revision ID. If the entity migration did not include the
* revision ID, the entity would get the next available revision ID (1 when
* migrating to a clean database). Then, when revisions are migrated after the
* entities, the revision IDs would almost certainly collide.
*
* The examples below contain simple node and node revision migrations. The
* examples use the EmbeddedDataSource source plugin for the sake of
* simplicity. The important part of both examples is the 'vid' property, which
* is the revision ID for nodes.
*
* Example of 'article' node migration, which must be executed before the
* 'article' revisions.
* @code
* id: custom_article_migration
* label: 'Custom article migration'
* source:
* plugin: embedded_data
* data_rows:
* -
* nid: 1
* vid: 2
* revision_timestamp: 1514661000
* revision_log: 'Second revision'
* title: 'Current title'
* content: '<p>Current content</p>'
* ids:
* nid:
* type: integer
* process:
* nid: nid
* vid: vid
* revision_timestamp: revision_timestamp
* revision_log: revision_log
* title: title
* 'body/0/value': content
* 'body/0/format':
* plugin: default_value
* default_value: basic_html
* destination:
* plugin: entity:node
* default_bundle: article
* @endcode
*
* Example of the corresponding node revision migration, which must be executed
* after the above migration.
* @code
* id: custom_article_revision_migration
* label: 'Custom article revision migration'
* source:
* plugin: embedded_data
* data_rows:
* -
* nid: 1
* vid: 1
* revision_timestamp: 1514660000
* revision_log: 'First revision'
* title: 'Previous title'
* content: '<p>Previous content</p>'
* ids:
* nid:
* type: integer
* process:
* nid:
* plugin: migration_lookup
* migration: custom_article_migration
* source: nid
* vid: vid
* revision_timestamp: revision_timestamp
* revision_log: revision_log
* title: title
* 'body/0/value': content
* 'body/0/format':
* plugin: default_value
* default_value: basic_html
* destination:
* plugin: entity_revision:node
* default_bundle: article
* migration_dependencies:
* required:
* - custom_article_migration
* @endcode
*/
#[MigrateDestination(
id: 'entity_revision',
deriver: MigrateEntityRevision::class
)]
class EntityRevision extends EntityContentBase {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, AccountSwitcherInterface $account_switcher) {
$plugin_definition += [
'label' => new TranslatableMarkup('@entity_type revisions', ['@entity_type' => $storage->getEntityType()->getSingularLabel()]),
];
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_field_manager, $field_type_manager, $account_switcher);
}
/**
* Gets the entity.
*
* @param \Drupal\migrate\Row $row
* The row object.
* @param array $old_destination_id_values
* The old destination IDs.
*
* @return \Drupal\Core\Entity\EntityInterface|false
* The entity or false if it can not be created.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
$revision_id = $old_destination_id_values ?
reset($old_destination_id_values) :
$row->getDestinationProperty($this->getKey('revision'));
$entity = NULL;
if (!empty($revision_id)) {
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = $this->storage;
if ($entity = $storage->loadRevision($revision_id)) {
$entity->setNewRevision(FALSE);
}
}
if ($entity === NULL) {
$entity_id = $row->getDestinationProperty($this->getKey('id'));
$entity = $this->storage->load($entity_id);
// If we fail to load the original entity something is wrong and we need
// to return immediately.
if (!$entity) {
return FALSE;
}
$entity->enforceIsNew(FALSE);
$entity->setNewRevision(TRUE);
}
// We need to update the entity, so that the destination row IDs are
// correct.
$entity = $this->updateEntity($entity, $row);
$entity->isDefaultRevision(FALSE);
return $entity;
}
/**
* {@inheritdoc}
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = []) {
$entity->setSyncing(TRUE);
$entity->save();
return [$entity->getRevisionId()];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids = [];
$revision_key = $this->getKey('revision');
if (!$revision_key) {
throw new MigrateException(sprintf('The "%s" entity type does not support revisions.', $this->storage->getEntityTypeId()));
}
$ids[$revision_key] = $this->getDefinitionFromEntity($revision_key);
if ($this->isTranslationDestination()) {
$langcode_key = $this->getKey('langcode');
if (!$langcode_key) {
throw new MigrateException(sprintf('The "%s" entity type does not support translations.', $this->storage->getEntityTypeId()));
}
$ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key);
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function getHighestId() {
$values = $this->storage->getQuery()
->accessCheck(FALSE)
->allRevisions()
->sort($this->getKey('revision'), 'DESC')
->range(0, 1)
->execute();
// The array keys are the revision IDs.
// The array contains only one entry, so we can use key().
return (int) key($values);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
/**
* Provides entity view mode destination plugin.
*
* See EntityConfigBase for the available configuration options.
* @see \Drupal\migrate\Plugin\migrate\destination\EntityConfigBase
*
* Example:
*
* @code
* source:
* plugin: d7_view_mode
* process:
* mode: view_mode
* label: view_mode
* targetEntityType: entity_type
* destination:
* plugin: entity:entity_view_mode
* @endcode
*
* This will add the results of the process ("mode", "label" and
* "targetEntityType") to an "entity_view_mode" entity.
*/
#[MigrateDestination('entity:entity_view_mode')]
class EntityViewMode extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['targetEntityType']['type'] = 'string';
$ids['mode']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
$destination_identifier = implode('.', $destination_identifier);
parent::rollback([$destination_identifier]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Row;
/**
* Provides null destination plugin.
*/
#[MigrateDestination(
id: 'null',
requirements_met: FALSE
)]
class NullDestination extends DestinationBase {
/**
* {@inheritdoc}
*/
public function getIds() {
return [];
}
/**
* {@inheritdoc}
*/
public function fields() {
return [];
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
/**
* This class imports one component of an entity display.
*
* Destination properties expected in the imported row:
* - entity_type: The entity type ID.
* - bundle: The entity bundle.
* - view_mode: The machine name of the view mode.
* - field_name: The machine name of the field to be imported into the display.
* - options: (optional) An array of options for displaying the field in this
* view mode.
*
* Examples:
*
* @code
* source:
* constants:
* entity_type: user
* bundle: user
* view_mode: default
* field_name: user_picture
* type: image
* options:
* label: hidden
* settings:
* image_style: ''
* image_link: content
* process:
* entity_type: 'constants/entity_type'
* bundle: 'constants/bundle'
* view_mode: 'constants/view_mode'
* field_name: 'constants/field_name'
* type: 'constants/type'
* options: 'constants/options'
* 'options/type': '@type'
* destination:
* plugin: component_entity_display
* @endcode
*
* This will add the "user_picture" image field to the "default" view mode of
* the "user" bundle of the "user" entity type with options as defined by the
* "options" constant, for example the label will be hidden.
*/
#[MigrateDestination('component_entity_display')]
class PerComponentEntityDisplay extends ComponentEntityDisplayBase {
const MODE_NAME = 'view_mode';
/**
* {@inheritdoc}
*/
protected function getEntity($entity_type, $bundle, $view_mode) {
return $this->entityDisplayRepository->getViewDisplay($entity_type, $bundle, $view_mode);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
/**
* This class imports one component of an entity form display.
*
* Destination properties expected in the imported row:
* - entity_type: The entity type ID.
* - bundle: The entity bundle.
* - form_mode: The machine name of the form mode.
* - field_name: The machine name of the field to be imported into the display.
* - options: (optional) An array of options for displaying the field in this
* form mode.
*
* Examples:
*
* @code
* source:
* constants:
* entity_type: node
* field_name: comment
* form_mode: default
* options:
* type: comment_default
* weight: 20
* process:
* entity_type: 'constants/entity_type'
* field_name: 'constants/field_name'
* form_mode: 'constants/form_mode'
* options: 'constants/options'
* bundle: node_type
* destination:
* plugin: component_entity_form_display
* @endcode
*
* This will add a "comment" field on the "default" form mode of the "node"
* entity type with options defined by the "options" constant.
*/
#[MigrateDestination('component_entity_form_display')]
class PerComponentEntityFormDisplay extends ComponentEntityDisplayBase {
const MODE_NAME = 'form_mode';
/**
* {@inheritdoc}
*/
protected function getEntity($entity_type, $bundle, $form_mode) {
return $this->entityDisplayRepository->getFormDisplay($entity_type, $bundle, $form_mode);
}
}

View File

@@ -0,0 +1,221 @@
<?php
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;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Defines the null ID map implementation.
*
* This serves as a dummy in order to not store anything.
*/
#[PluginID('null')]
class NullIdMap extends PluginBase implements MigrateIdMapInterface {
/**
* {@inheritdoc}
*/
public function setMessage(MigrateMessageInterface $message) {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function getRowBySource(array $source_id_values) {
return [];
}
/**
* {@inheritdoc}
*/
public function getRowByDestination(array $destination_id_values) {
return [];
}
/**
* {@inheritdoc}
*/
public function getRowsNeedingUpdate($count) {
return 0;
}
/**
* {@inheritdoc}
*/
public function lookupSourceId(array $destination_id_values) {
return [];
}
/**
* {@inheritdoc}
*/
public function lookupDestinationIds(array $source_id_values) {
return [];
}
/**
* {@inheritdoc}
*/
public function saveIdMapping(Row $row, array $destination_id_values, $source_row_status = MigrateIdMapInterface::STATUS_IMPORTED, $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE) {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR) {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function getMessages(array $source_id_values = [], $level = NULL) {
return new \ArrayIterator([]);
}
/**
* {@inheritdoc}
*/
public function prepareUpdate() {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function processedCount() {
return 0;
}
/**
* {@inheritdoc}
*/
public function importedCount() {
return 0;
}
/**
* {@inheritdoc}
*/
public function updateCount() {
return 0;
}
/**
* {@inheritdoc}
*/
public function errorCount() {
return 0;
}
/**
* {@inheritdoc}
*/
public function messageCount() {
return 0;
}
/**
* {@inheritdoc}
*/
public function delete(array $source_id_values, $messages_only = FALSE) {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function deleteDestination(array $destination_id_values) {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function setUpdate(array $source_id_values) {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function clearMessages() {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function destroy() {
// Do nothing.
}
/**
* {@inheritdoc}
*/
public function currentDestination() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function currentSource() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getQualifiedMapTableName() {
return '';
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function rewind() {
return NULL;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function current() {
return NULL;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function key() {
return '';
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function next() {
return NULL;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function valid() {
return FALSE;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Builds an array based on the key and value configuration.
*
* The array_build plugin builds a single associative array by extracting keys
* and values from each array in the input value, which is expected to be an
* array of arrays. The keys of the returned array will be determined by the
* 'key' configuration option, and the values will be determined by the 'value'
* option.
*
* Available configuration keys
* - key: The key used to lookup a value in the source arrays to be used as
* a key in the destination array.
* - value: The key used to lookup a value in the source arrays to be used as
* a value in the destination array.
*
* Example:
*
* Consider the migration of language negotiation by domain.
* The source is an array of all the languages:
*
* @code
* languages: Array
* (
* [0] => Array
* (
* [language] => en
* ...
* [domain] => http://example.com
* )
* [1] => Array
* (
* [language] => fr
* ...
* [domain] => http://fr.example.com
* )
* ...
* @endcode
*
* The destination should be an array of all the domains keyed by their
* language code:
*
* @code
* domains: Array
* (
* [en] => http://example.com
* [fr] => http://fr.example.com
* ...
* @endcode
*
* The array_build process plugin would be used like this:
*
* @code
* process:
* domains:
* plugin: array_build
* key: language
* value: domain
* source: languages
* @endcode
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess(
id: "array_build",
handle_multiples: TRUE,
)]
class ArrayBuild extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$new_value = [];
foreach ((array) $value as $old_value) {
// Checks that $old_value is an array.
if (!is_array($old_value)) {
throw new MigrateException("The input should be an array of arrays");
}
// Checks that the key exists.
if (!array_key_exists($this->configuration['key'], $old_value)) {
throw new MigrateException("The key '" . $this->configuration['key'] . "' does not exist");
}
// Checks that the value exists.
if (!array_key_exists($this->configuration['value'], $old_value)) {
throw new MigrateException("The key '" . $this->configuration['value'] . "' does not exist");
}
$new_value[$old_value[$this->configuration['key']]] = $old_value[$this->configuration['value']];
}
return $new_value;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Passes the source value to a callback.
*
* The callback process plugin allows simple processing of the value, such as
* strtolower(). To pass more than one argument, pass an array as the source
* and set the unpack_source option.
*
* Available configuration keys:
* - callable: The name of the callable method.
* - unpack_source: (optional) Whether to interpret the source as an array of
* arguments.
*
* Examples:
*
* @code
* process:
* destination_field:
* plugin: callback
* callable: mb_strtolower
* source: source_field
* @endcode
*
* An example where the callable is a static method in a class:
*
* @code
* process:
* destination_field:
* plugin: callback
* callable:
* - '\Drupal\Component\Utility\Unicode'
* - ucfirst
* source: source_field
* @endcode
*
* An example where the callback accepts no arguments:
*
* @code
* process:
* time:
* plugin: callback
* callable: time
* unpack_source: true
* source: [ ]
* @endcode
*
* An example where the callback accepts more than one argument:
*
* @code
* source:
* plugin: source_plugin_goes_here
* constants:
* slash: /
* process:
* field_link_url:
* plugin: callback
* callable: rtrim
* unpack_source: true
* source:
* - url
* - constants/slash
* @endcode
*
* This will remove the trailing '/', if any, from a URL.
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess('callback')]
class Callback extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
if (!isset($configuration['callable'])) {
throw new \InvalidArgumentException('The "callable" must be set.');
}
elseif (!is_callable($configuration['callable'])) {
throw new \InvalidArgumentException('The "callable" must be a valid function or method.');
}
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!empty($this->configuration['unpack_source'])) {
if (!is_array($value)) {
throw new MigrateException(sprintf("When 'unpack_source' is set, the source must be an array. Instead it was of type '%s'", gettype($value)));
}
return call_user_func($this->configuration['callable'], ...$value);
}
return call_user_func($this->configuration['callable'], $value);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Concatenates a set of strings.
*
* The concat plugin is used to concatenate strings. For example, imploding a
* set of strings into a single string.
*
* Available configuration keys:
* - delimiter: (optional) A delimiter, or glue string, to insert between the
* strings.
*
* Examples:
*
* @code
* process:
* new_text_field:
* plugin: concat
* source:
* - foo
* - bar
* @endcode
*
* This will set new_text_field to the concatenation of the 'foo' and 'bar'
* source values. For example, if the 'foo' property is "Rosa" and the 'bar'
* property is "Parks", new_text_field will be "RosaParks".
*
* You can also specify a delimiter.
*
* @code
* process:
* new_text_field:
* plugin: concat
* source:
* - foo
* - bar
* delimiter: /
* @endcode
*
* This will set new_text_field to the concatenation of the 'foo' source value,
* the delimiter and the 'bar' source value. For example, using the values above
* and "/" as the delimiter, if the 'foo' property is "Rosa" and the 'bar'
* property is "Rosa", new_text_field will be "Rosa/Parks".
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess(
id: "concat",
handle_multiples: TRUE,
)]
class Concat extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_array($value)) {
$delimiter = $this->configuration['delimiter'] ?? '';
return implode($delimiter, $value);
}
else {
throw new MigrateException(sprintf('%s is not an array', var_export($value, TRUE)));
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* Returns a given default value if the input is empty.
*
* The default_value process plugin provides the ability to set a fixed default
* value. The plugin returns a default value if the input value is considered
* empty (NULL, FALSE, 0, '0', an empty string, or an empty array). The strict
* configuration key can be used to set the default only when the incoming
* value is NULL.
*
* Available configuration keys:
* - default_value: The fixed default value to apply.
* - strict: (optional) Use strict value checking. Defaults to false.
* - FALSE: Apply default when input value is empty().
* - TRUE: Apply default when input value is NULL.
*
* Example:
*
* @code
* process:
* uid:
* -
* plugin: migration_lookup
* migration: users
* source: author
* no_stub: true
* -
* plugin: default_value
* default_value: 44
* @endcode
*
* This will look up the source value of author in the users migration and if
* not found, set the destination property uid to 44.
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess(
id: "default_value",
handle_multiples: TRUE,
)]
class DefaultValue extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!empty($this->configuration['strict'])) {
return $value ?? $this->configuration['default_value'];
}
return $value ?: $this->configuration['default_value'];
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use GuzzleHttp\ClientInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Downloads a file from a HTTP(S) remote location into the local file system.
*
* The source value is an array of two values:
* - source URL, e.g. 'http://www.example.com/img/foo.img'
* - destination URI, e.g. 'public://images/foo.img'
*
* Available configuration keys:
* - file_exists: (optional) Replace behavior when the destination file already
* exists:
* - 'replace' - (default) Replace the existing file.
* - 'rename' - Append _{incrementing number} until the filename is
* unique.
* - 'use existing' - Do nothing and return FALSE.
* - guzzle_options: (optional)
* @link http://docs.guzzlephp.org/en/latest/request-options.html Array of request options for Guzzle. @endlink
*
* Examples:
*
* @code
* process:
* path_to_file:
* plugin: download
* source:
* - source_url
* - destination_uri
* @endcode
*
* This will download source_url to destination_uri.
*
* @code
* process:
* uri:
* plugin: download
* source:
* - source_url
* - destination_uri
* file_exists: rename
* # other fields ...
* destination:
* plugin: entity:file
* @endcode
*
* This will download source_url to destination_uri and ensure that the
* destination URI is unique. If a file with the same name exists at the
* destination, a numbered suffix like '_0' will be appended to make it unique.
* The destination URI is saved in a file entity.
*/
#[MigrateProcess('download')]
class Download extends FileProcessBase implements ContainerFactoryPluginInterface {
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The Guzzle HTTP Client service.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* Constructs a download process plugin.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param array $plugin_definition
* The plugin definition.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, FileSystemInterface $file_system, ClientInterface $http_client) {
$configuration += [
'guzzle_options' => [],
];
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->fileSystem = $file_system;
$this->httpClient = $http_client;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('file_system'),
$container->get('http_client')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// If we're stubbing a file entity, return a uri of NULL so it will get
// stubbed by the general process.
if ($row->isStub()) {
return NULL;
}
[$source, $destination] = $value;
// Modify the destination filename if necessary.
$final_destination = $this->fileSystem->getDestinationFilename($destination, $this->configuration['file_exists']);
// Reuse if file exists.
if (!$final_destination) {
return $destination;
}
// Try opening the file first, to avoid calling prepareDirectory()
// unnecessarily. We're suppressing fopen() errors because we want to try
// to prepare the directory before we give up and fail.
$destination_stream = @fopen($final_destination, 'w');
if (!$destination_stream) {
// If fopen didn't work, make sure there's a writable directory in place.
$dir = $this->fileSystem->dirname($final_destination);
if (!$this->fileSystem->prepareDirectory($dir, FileSystemInterface:: CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
throw new MigrateException("Could not create or write to directory '$dir'");
}
// Let's try that fopen again.
$destination_stream = @fopen($final_destination, 'w');
if (!$destination_stream) {
throw new MigrateException("Could not write to file '$final_destination'");
}
}
// Stream the request body directly to the final destination stream.
$this->configuration['guzzle_options']['sink'] = $destination_stream;
try {
// Make the request. Guzzle throws an exception for anything but 200.
$this->httpClient->get($source, $this->configuration['guzzle_options']);
}
catch (\Exception $e) {
throw new MigrateException("{$e->getMessage()} ($source)");
}
if (is_resource($destination_stream)) {
fclose($destination_stream);
}
return $final_destination;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* This plugin checks if a given entity exists.
*
* Example usage with configuration:
* @code
* field_tags:
* plugin: entity_exists
* source: tid
* entity_type: taxonomy_term
* @endcode
*/
#[MigrateProcess('entity_exists')]
class EntityExists extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* EntityExists constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param $storage
* The entity storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->storage = $storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage($configuration['entity_type'])
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_array($value)) {
$value = reset($value);
}
$entity = $this->storage->load($value);
if ($entity instanceof EntityInterface) {
return $entity->id();
}
return FALSE;
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* Splits the source string into an array of strings, using a delimiter.
*
* This plugin creates an array of strings by splitting the source parameter on
* boundaries formed by the delimiter.
*
* Available configuration keys:
* - source: The source string.
* - limit: (optional)
* - If limit is set and positive, the returned array will contain a maximum
* of limit elements with the last element containing the rest of string.
* - If limit is set and negative, all components except the last -limit are
* returned.
* - If the limit parameter is zero, then this is treated as 1.
* - delimiter: The boundary string.
* - strict: (optional) When this boolean is TRUE, the source should be strictly
* a string. If FALSE is passed, the source value is casted to a string before
* being split. Also, in this case, the values casting to empty strings are
* converted to empty arrays, instead of an array with a single empty string
* item ['']. Defaults to TRUE.
*
* Example:
*
* @code
* process:
* bar:
* plugin: explode
* source: foo
* delimiter: /
* @endcode
*
* If foo is "node/1", then bar will be ['node', '1']. The PHP equivalent of
* this would be:
*
* @code
* $bar = explode('/', $foo);
* @endcode
*
* @code
* process:
* bar:
* plugin: explode
* source: foo
* limit: 2
* delimiter: /
* @endcode
*
* If foo is "node/1/edit", then bar will be ['node', '1/edit']. The PHP
* equivalent of this would be:
*
* @code
* $bar = explode('/', $foo, 2);
* @endcode
*
* If the 'strict' configuration is set to FALSE, the input value is casted to a
* string before being spilt:
*
* @code
* process:
* bar:
* plugin: explode
* source: foo
* delimiter: /
* strict: false
* @endcode
*
* If foo is 123 (as integer), then bar will be ['123']. If foo is TRUE, then
* bar will be ['1']. The PHP equivalent of this would be:
*
* @code
* $bar = explode('/', (string) 123);
* $bar = explode('/', (string) TRUE);
* @endcode
*
* If the 'strict' configuration is set to FALSE, the source value casting to
* an empty string are converted to an empty array. For example, with the last
* configuration, if foo is '', NULL or FALSE, then bar will be [].
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess('explode')]
class Explode extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (empty($this->configuration['delimiter'])) {
throw new MigrateException('delimiter is empty');
}
$strict = array_key_exists('strict', $this->configuration) ? $this->configuration['strict'] : TRUE;
if ($strict && !is_string($value)) {
throw new MigrateException(sprintf('%s is not a string', var_export($value, TRUE)));
}
elseif (!$strict) {
// Check if the incoming value can cast to a string.
$original = $value;
if (!is_string($original) && ($original != ($value = @strval($value)))) {
throw new MigrateException(sprintf('%s cannot be casted to a string', var_export($original, TRUE)));
}
// Empty strings should be exploded to empty arrays.
if ($value === '') {
return [];
}
}
$limit = $this->configuration['limit'] ?? PHP_INT_MAX;
return explode($this->configuration['delimiter'], $value, $limit);
}
/**
* {@inheritdoc}
*/
public function multiple() {
return TRUE;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Variable;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* Extracts a value from an array.
*
* The extract process plugin is used to pull data from an input array, which
* may have multiple levels. One use case is extracting data from field arrays
* in previous versions of Drupal. For instance, in Drupal 7, a field array
* would be indexed first by language, then by delta, then finally a key such as
* 'value'.
*
* Available configuration keys:
* - source: The input value - must be an array.
* - index: The array of keys to access the value.
* - default: (optional) A default value to assign to the destination if the
* key does not exist.
*
* Examples:
*
* @code
* process:
* new_text_field:
* plugin: extract
* source: some_text_field
* index:
* - und
* - 0
* - value
* @endcode
*
* The PHP equivalent of this would be:
* @code
* $destination['new_text_field'] = $source['some_text_field']['und'][0]['value'];
* @endcode
* If a default value is specified, it will be returned if the index does not
* exist in the input array.
*
* @code
* plugin: extract
* source: some_text_field
* default: 'Default title'
* index:
* - title
* @endcode
*
* If $source['some_text_field']['title'] doesn't exist, then the plugin will
* return "Default title".
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess(
id: "extract",
handle_multiples: TRUE,
)]
class Extract extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!is_array($value)) {
throw new MigrateException(sprintf("Input should be an array, instead it was of type '%s'", gettype($value)));
}
$new_value = NestedArray::getValue($value, $this->configuration['index'], $key_exists);
if (!$key_exists) {
if (array_key_exists('default', $this->configuration)) {
$new_value = $this->configuration['default'];
}
else {
throw new MigrateException(sprintf("Array index missing, extraction failed for '%s'. Consider adding a `default` key to the configuration.", Variable::export($value)));
}
}
return $new_value;
}
}

View File

@@ -0,0 +1,262 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Copies or moves a local file from one place into another.
*
* The file can be moved, reused, or set to be automatically renamed if a
* duplicate exists.
*
* The source value is an indexed array of two values:
* - The source path or URI, e.g. '/path/to/foo.txt' or 'public://bar.txt'.
* - The destination URI, e.g. 'public://foo.txt'.
*
* Available configuration keys:
* - move: (optional) Boolean, if TRUE, move the file, otherwise copy the file.
* Defaults to FALSE.
* - file_exists: (optional) Replace behavior when the destination file already
* exists:
* - 'replace' - (default) Replace the existing file.
* - 'rename' - Append _{incrementing number} until the filename is
* unique.
* - 'use existing' - Do nothing and return FALSE.
*
* Examples:
*
* @code
* process:
* path_to_file:
* plugin: file_copy
* source:
* - /path/to/file.png
* - public://new/path/to/file.png
* @endcode
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess('file_copy')]
class FileCopy extends FileProcessBase implements ContainerFactoryPluginInterface {
/**
* The stream wrapper manager service.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* An instance of the download process plugin.
*
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
*/
protected $downloadPlugin;
/**
* Constructs a file_copy process plugin.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param array $plugin_definition
* The plugin definition.
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrappers
* The stream wrapper manager service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\migrate\Plugin\MigrateProcessInterface $download_plugin
* An instance of the download plugin for handling remote URIs.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system, MigrateProcessInterface $download_plugin) {
$configuration += [
'move' => FALSE,
];
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->streamWrapperManager = $stream_wrappers;
$this->fileSystem = $file_system;
$this->downloadPlugin = $download_plugin;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('stream_wrapper_manager'),
$container->get('file_system'),
$container->get('plugin.manager.migrate.process')->createInstance('download', $configuration)
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// If we're stubbing a file entity, return a URI of NULL so it will get
// stubbed by the general process.
if ($row->isStub()) {
return NULL;
}
[$source, $destination] = $value;
// If the source path or URI represents a remote resource, delegate to the
// download plugin.
if (!$this->isLocalUri($source)) {
return $this->downloadPlugin->transform($value, $migrate_executable, $row, $destination_property);
}
// Ensure the source file exists, if it's a local URI or path.
if (!file_exists($source)) {
throw new MigrateException("File '$source' does not exist");
}
// If the start and end file is exactly the same, there is nothing to do.
if ($this->isLocationUnchanged($source, $destination)) {
return $destination;
}
// Check if a writable directory exists, and if not try to create it.
$dir = $this->getDirectory($destination);
// If the directory exists and is writable, avoid
// \Drupal\Core\File\FileSystemInterface::prepareDirectory() call and write
// the file to destination.
if (!is_dir($dir) || !is_writable($dir)) {
if (!$this->fileSystem->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
throw new MigrateException("Could not create or write to directory '$dir'");
}
}
$final_destination = $this->writeFile($source, $destination, $this->configuration['file_exists']);
if ($final_destination) {
return $final_destination;
}
throw new MigrateException("File $source could not be copied to $destination");
}
/**
* Tries to move or copy a file.
*
* @param string $source
* The source path or URI.
* @param string $destination
* The destination path or URI.
* @param \Drupal\Core\File\FileExists|int $fileExists
* (optional) FileExists::Replace (default) or
* FileExists::Rename.
*
* @return string|bool
* File destination on success, FALSE on failure.
*/
protected function writeFile($source, $destination, FileExists|int $fileExists = FileExists::Replace) {
if (!$fileExists instanceof FileExists) {
// @phpstan-ignore-next-line
$fileExists = FileExists::fromLegacyInt($fileExists, __METHOD__);
}
// Check if there is a destination available for copying. If there isn't,
// it already exists at the destination and the replace flag tells us to not
// replace it. In that case, return the original destination.
if ($this->fileSystem->getDestinationFilename($destination, $fileExists) === FALSE) {
return $destination;
}
try {
if ($this->configuration['move']) {
return $this->fileSystem->move($source, $destination, $fileExists);
}
else {
return $this->fileSystem->copy($source, $destination, $fileExists);
}
}
catch (FileException $e) {
return FALSE;
}
}
/**
* Returns the directory component of a URI or path.
*
* For URIs like public://foo.txt, the full physical path of public://
* will be returned, since a scheme by itself will trip up certain file
* API functions (such as
* \Drupal\Core\File\FileSystemInterface::prepareDirectory()).
*
* @param string $uri
* The URI or path.
*
* @return string|false
* The directory component of the path or URI, or FALSE if it could not
* be determined.
*/
protected function getDirectory($uri) {
$dir = $this->fileSystem->dirname($uri);
if (str_ends_with($dir, '://')) {
return $this->fileSystem->realpath($dir);
}
return $dir;
}
/**
* Determines if the source and destination URIs represent identical paths.
*
* @param string $source
* The source URI.
* @param string $destination
* The destination URI.
*
* @return bool
* TRUE if the source and destination URIs refer to the same physical path,
* otherwise FALSE.
*/
protected function isLocationUnchanged($source, $destination) {
return $this->fileSystem->realpath($source) === $this->fileSystem->realpath($destination);
}
/**
* Determines if the given URI or path is considered local.
*
* A URI or path is considered local if it either has no scheme component,
* or the scheme is implemented by a stream wrapper which extends
* \Drupal\Core\StreamWrapper\LocalStream.
*
* @param string $uri
* The URI or path to test.
*
* @return bool
*/
protected function isLocalUri($uri) {
$scheme = StreamWrapperManager::getScheme($uri);
// The vfs scheme is vfsStream, which is used in testing. vfsStream is a
// simulated file system that exists only in memory, but should be treated
// as a local resource.
if ($scheme == 'vfs') {
$scheme = FALSE;
}
return $scheme === FALSE || $this->streamWrapperManager->getViaScheme($scheme) instanceof LocalStream;
}
}

Some files were not shown because too many files have changed in this diff Show More