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,13 @@
name: Migrate Drupal
type: module
description: 'Provides a framework to migrate data from previous versions of Drupal into the site.'
package: Migration
# version: VERSION
dependencies:
- drupal:migrate
- drupal:phpass
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains install and update functions for Migrate Drupal.
*/
/**
* Implements hook_update_last_removed().
*/
function migrate_drupal_update_last_removed() {
return 8901;
}

View File

@@ -0,0 +1,141 @@
<?php
/**
* @file
* Provides migration from other Drupal sites.
*/
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\RequirementsInterface;
use Drupal\migrate_drupal\NodeMigrateType;
/**
* Implements hook_help().
*/
function migrate_drupal_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.migrate_drupal':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Migrate Drupal module provides a framework based on the <a href=":migrate">Migrate module</a> to facilitate migration from a Drupal (6, 7, or 8) site to your website. It does not provide a user interface. For more information, see the <a href=":migrate_drupal">online documentation for the Migrate Drupal module</a>.', [':migrate' => Url::fromRoute('help.page', ['name' => 'migrate'])->toString(), ':migrate_drupal' => 'https://www.drupal.org/documentation/modules/migrate_drupal']) . '</p>';
return $output;
}
}
/**
* Implements hook_migration_plugins_alter().
*/
function migrate_drupal_migration_plugins_alter(array &$definitions) {
$module_handler = \Drupal::service('module_handler');
$migration_plugin_manager = \Drupal::service('plugin.manager.migration');
// This is why the deriver can't do this: the 'd6_taxonomy_vocabulary'
// definition is not available to the deriver as it is running inside
// getDefinitions().
if (isset($definitions['d6_taxonomy_vocabulary'])) {
$vocabulary_migration_definition = [
'source' => [
'ignore_map' => TRUE,
'plugin' => 'd6_taxonomy_vocabulary',
],
'destination' => [
'plugin' => 'null',
],
'idMap' => [
'plugin' => 'null',
],
];
$vocabulary_migration = $migration_plugin_manager->createStubMigration($vocabulary_migration_definition);
$translation_active = $module_handler->moduleExists('content_translation');
try {
$source_plugin = $vocabulary_migration->getSourcePlugin();
if ($source_plugin instanceof RequirementsInterface) {
$source_plugin->checkRequirements();
}
$executable = new MigrateExecutable($vocabulary_migration);
$process = ['vid' => $definitions['d6_taxonomy_vocabulary']['process']['vid']];
foreach ($source_plugin as $row) {
$executable->processRow($row, $process);
$source_vid = $row->getSourceProperty('vid');
$plugin_ids = [
'd6_term_node:' . $source_vid,
'd6_term_node_revision:' . $source_vid,
];
if ($translation_active) {
$plugin_ids[] = 'd6_term_node_translation:' . $source_vid;
}
foreach (array_intersect($plugin_ids, array_keys($definitions)) as $plugin_id) {
// Match the field name derivation in d6_vocabulary_field.yml.
$field_name = substr('field_' . $row->getDestinationProperty('vid'), 0, 32);
// The Forum module is expecting 'taxonomy_forums' as the field name
// for the forum nodes. The 'forum_vocabulary' source property is
// evaluated in Drupal\taxonomy\Plugin\migrate\source\d6\Vocabulary
// and is set to true if the vocabulary vid being migrated is the
// same as the one in the 'forum_nav_vocabulary' variable on the
// source site.
$destination_vid = $row->getSourceProperty('forum_vocabulary') ? 'taxonomy_forums' : $field_name;
$definitions[$plugin_id]['process'][$destination_vid] = 'tid';
}
}
}
catch (RequirementsException $e) {
// This code currently runs whenever the definitions are being loaded and
// if you have a Drupal 7 source site then the requirements will not be
// met for the d6_taxonomy_vocabulary migration.
}
catch (DatabaseExceptionWrapper $e) {
// When the definitions are loaded it is possible the tables will not
// exist.
}
}
if (!$module_handler->moduleExists('node')) {
return;
}
$connection = \Drupal::database();
// We need to get the version of the source database in order to check
// if the classic or complete node tables have been used in a migration.
if (isset($definitions['system_site'])) {
// Use the source plugin of the system_site migration to get the
// database connection.
$migration = $definitions['system_site'];
/** @var \Drupal\migrate\Plugin\migrate\source\SqlBase $source_plugin */
$source_plugin = $migration_plugin_manager->createStubMigration($migration)
->getSourcePlugin();
try {
$source_connection = $source_plugin->getDatabase();
$version = NodeMigrateType::getLegacyDrupalVersion($source_connection);
}
catch (\Exception $e) {
\Drupal::messenger()
->addError(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', ['%error' => $e->getMessage()]));
}
}
// If this is a complete node migration then for all migrations, except the
// classic node migrations, replace any dependency on a classic node migration
// with a dependency on the complete node migration.
if (NodeMigrateType::getNodeMigrateType($connection, $version ?? FALSE) === NodeMigrateType::NODE_MIGRATE_TYPE_COMPLETE) {
$classic_migration_match = '/d([67])_(node|node_translation|node_revision|node_entity_translation)($|:.*)/';
$replace_with_complete_migration = function (&$value, $key, $classic_migration_match) {
if (is_string($value)) {
$value = preg_replace($classic_migration_match, 'd$1_node_complete$3', $value);
}
};
foreach ($definitions as &$definition) {
$is_node_classic_migration = preg_match($classic_migration_match, $definition['id']);
if (!$is_node_classic_migration && isset($definition['migration_dependencies'])) {
array_walk_recursive($definition['migration_dependencies'], $replace_with_complete_migration, $classic_migration_match);
}
}
}
}

View File

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

View File

@@ -0,0 +1,25 @@
services:
plugin.manager.migrate.field:
class: Drupal\migrate_drupal\Plugin\MigrateFieldPluginManager
arguments:
- field
- '@container.namespaces'
- '@cache.discovery'
- '@module_handler'
- '\Drupal\migrate_drupal\Attribute\MigrateField'
- '\Drupal\migrate_drupal\Annotation\MigrateField'
Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface: '@plugin.manager.migrate.field'
logger.channel.migrate_drupal:
parent: logger.channel_base
arguments: ['migrate_drupal']
migrate_drupal.field_discovery:
class: Drupal\migrate_drupal\FieldDiscovery
arguments:
- '@plugin.manager.migrate.field'
- '@plugin.manager.migration'
- '@logger.channel.migrate_drupal'
Drupal\migrate_drupal\FieldDiscoveryInterface: '@migrate_drupal.field_discovery'
migrate_drupal.migration_state:
class: Drupal\migrate_drupal\MigrationState
arguments: ['@plugin.manager.migrate.field', '@module_handler', '@messenger', '@string_translation']
Drupal\migrate_drupal\MigrationState: '@migrate_drupal.migration_state'

View File

@@ -0,0 +1,94 @@
# The modules listed here do not have an migration. A status of finished is
# assigned so that they appear in the will not be upgraded list on the Review
# form.
# cspell:ignore blogapi calendarsignup ctools datepicker ddblock entityreference
# cspell:ignore filefield imageapi imagefield imagecache imagemagick multigroup
# cspell:ignore multiupload nodeaccess openid
finished:
6:
# The following do not have an upgrade path.
blogapi: core
calendarsignup: core
content_copy: core
content_multigroup: core
content_permissions: core
date_api: core
date_locale: core
date_php4: core
date_popup: core
date_repeat: core
date_timezone: core
date_tools: core
datepicker: core
ddblock: core
event: core
fieldgroup: core
filefield_meta: core
help: core
imageapi: core
imageapi_gd: core
imageapi_imagemagick: core
imagecache_ui: core
nodeaccess: core
number: core
openid: core
php: core
ping: core
poll: core
throttle: core
# @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
tracker: core
translation: core
trigger: core
variable: core
variable_admin: core
views_export: core
views_ui: core
7:
# The following do not need have an upgrade path.
bulk_export: core
contextual: core
ctools: core
ctools_access_ruleset: core
ctools_ajax_sample: core
ctools_custom_content: core
dashboard: core
date_all_day: core
date_api: core
date_context: core
date_migrate: core
date_popup: core
date_repeat: core
date_repeat_field: core
date_tools: core
date_views: core
entity: core
entity_feature: core
entity_token: core
entityreference: core
field_ui: core
help: core
multiupload_filefield_widget: core
multiupload_imagefield_widget: core
node_reference: core
openid: core
overlay: core
page_manager: core
php: core
poll: core
search_embedded_form: core
search_extra_type: core
search_node_tags: core
simpletest: core
stylizer: core
term_depth: core
title: core
toolbar: core
translation: core
trigger: core
user_reference: core
views_content: core
views_ui: core
not_finished:
7:
references_uuid: core

View File

@@ -0,0 +1,85 @@
<?php
namespace Drupal\migrate_drupal\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a field plugin annotation object.
*
* Field plugins are responsible for handling the migration of custom fields
* (provided by Field API in Drupal 7) to Drupal 8. They are allowed to alter
* fieldable entity migrations when these migrations are being generated, and
* can compute destination field types for individual fields during the actual
* migration process.
*
* Plugin Namespace: Plugin\migrate\field
*
* @Annotation
*/
class MigrateField extends Plugin {
/**
* {@inheritdoc}
*/
public function __construct($values) {
parent::__construct($values);
// Provide default value for core property, in case it's missing.
if (empty($this->definition['core'])) {
$this->definition['core'] = [6];
}
}
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* Map of D6 and D7 field types to D8 field type plugin IDs.
*
* @var string[]
*/
public $type_map = [];
/**
* The Drupal core version(s) this plugin applies to.
*
* @var int[]
*/
public $core;
/**
* Identifies the system providing the data the field plugin will read.
*
* The source_module is expected to be the name of a Drupal module that must
* be installed in the source database.
*
* @var string
*/
public $source_module;
/**
* Identifies the system handling the data the destination plugin will write.
*
* The destination_module is expected to be the name of a Drupal module on the
* destination site that must be installed.
*
* @var string
*/
public $destination_module;
/**
* The weight of this plugin relative to other plugins.
*
* The weight of this plugin relative to other plugins servicing the same
* field type and core version. The lowest weighted applicable plugin will be
* used for each field.
*
* @var int
*/
public $weight = 0;
}

View File

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

View File

@@ -0,0 +1,344 @@
<?php
namespace Drupal\migrate_drupal;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate\Plugin\RequirementsInterface;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
use Psr\Log\LoggerInterface;
/**
* Provides field discovery for Drupal 6 & 7 migrations.
*/
class FieldDiscovery implements FieldDiscoveryInterface {
/**
* An array of already discovered field plugin information.
*
* @var array
*/
protected $fieldPluginCache;
/**
* The field plugin manager.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
*/
protected $fieldPluginManager;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* The logger channel service.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* A cache of discovered fields.
*
* It is an array of arrays. If the entity type is bundleable, a third level
* of arrays is added to account for fields discovered at the bundle level.
*
* [{core}][{entity_type}][{bundle}]
*
* @var array
*/
protected $discoveredFieldsCache = [];
/**
* An array of bundle keys, keyed by drupal core version.
*
* In Drupal 6, only nodes were fieldable, and the bundles were called
* 'type_name'. In Drupal 7, everything became entities, and the more
* generic 'bundle' was used.
*
* @var array
*/
protected $bundleKeys = [
FieldDiscoveryInterface::DRUPAL_6 => 'type_name',
FieldDiscoveryInterface::DRUPAL_7 => 'bundle',
];
/**
* An array of source plugin ids, keyed by Drupal core version.
*
* @var array
*/
protected $sourcePluginIds = [
FieldDiscoveryInterface::DRUPAL_6 => 'd6_field_instance',
FieldDiscoveryInterface::DRUPAL_7 => 'd7_field_instance',
];
/**
* An array of supported Drupal core versions.
*
* @var array
*/
protected $supportedCoreVersions = [
FieldDiscoveryInterface::DRUPAL_6,
FieldDiscoveryInterface::DRUPAL_7,
];
/**
* Constructs a FieldDiscovery object.
*
* @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_plugin_manager
* The field plugin manager.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
* @param \Psr\Log\LoggerInterface $logger
* The logger channel service.
*/
public function __construct(MigrateFieldPluginManagerInterface $field_plugin_manager, MigrationPluginManagerInterface $migration_plugin_manager, LoggerInterface $logger) {
$this->fieldPluginManager = $field_plugin_manager;
$this->migrationPluginManager = $migration_plugin_manager;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function addAllFieldProcesses(MigrationInterface $migration) {
$core = $this->getCoreVersion($migration);
$fields = $this->getAllFields($core);
foreach ($fields as $entity_type_id => $bundle) {
$this->addEntityFieldProcesses($migration, $entity_type_id);
}
}
/**
* {@inheritdoc}
*/
public function addEntityFieldProcesses(MigrationInterface $migration, $entity_type_id) {
$core = $this->getCoreVersion($migration);
$fields = $this->getAllFields($core);
if (!empty($fields[$entity_type_id]) && is_array($fields[$entity_type_id])) {
foreach ($fields[$entity_type_id] as $bundle => $fields) {
$this->addBundleFieldProcesses($migration, $entity_type_id, $bundle);
}
}
}
/**
* {@inheritdoc}
*/
public function addBundleFieldProcesses(MigrationInterface $migration, $entity_type_id, $bundle) {
$core = $this->getCoreVersion($migration);
$fields = $this->getAllFields($core);
$plugin_definition = $migration->getPluginDefinition();
if (empty($fields[$entity_type_id][$bundle])) {
return;
}
$bundle_fields = $fields[$entity_type_id][$bundle];
foreach ($bundle_fields as $field_name => $field_info) {
$plugin = $this->getFieldPlugin($field_info['type'], $migration);
if ($plugin) {
$method = $plugin_definition['field_plugin_method'] ?? 'defineValueProcessPipeline';
call_user_func_array([
$plugin,
$method,
], [
$migration,
$field_name,
$field_info,
]);
}
else {
// Default to a get process plugin if this is a value migration.
if ((empty($plugin_definition['field_plugin_method']) || $plugin_definition['field_plugin_method'] === 'defineValueProcessPipeline')) {
$migration->setProcessOfProperty($field_name, $field_name);
}
}
}
}
/**
* Returns the appropriate field plugin for a given field type.
*
* @param string $field_type
* The field type.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to retrieve the plugin for.
*
* @return \Drupal\migrate_drupal\Plugin\MigrateFieldInterface|bool
* The appropriate field plugin to process this field type.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \InvalidArgumentException
*/
protected function getFieldPlugin($field_type, MigrationInterface $migration) {
$core = $this->getCoreVersion($migration);
if (!isset($this->fieldPluginCache[$core][$field_type])) {
try {
$plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, ['core' => $core], $migration);
$plugin = $this->fieldPluginManager->createInstance($plugin_id, ['core' => $core], $migration);
}
catch (PluginNotFoundException $ex) {
$plugin = FALSE;
}
$this->fieldPluginCache[$core][$field_type] = $plugin;
}
return $this->fieldPluginCache[$core][$field_type];
}
/**
* Gets all field information related to this migration.
*
* @param string $core
* The Drupal core version to get fields for.
*
* @return array
* A multidimensional array of source data from the relevant field instance
* migration, keyed first by entity type, then by bundle and finally by
* field name.
*/
protected function getAllFields($core) {
if (empty($this->discoveredFieldsCache[$core])) {
$this->discoveredFieldsCache[$core] = [];
$source_plugin = $this->getSourcePlugin($core);
foreach ($source_plugin as $row) {
/** @var \Drupal\migrate\Row $row */
if ($core === FieldDiscoveryInterface::DRUPAL_7) {
$entity_type_id = $row->get('entity_type');
}
else {
$entity_type_id = 'node';
}
$bundle = $row->getSourceProperty($this->bundleKeys[$core]);
$this->discoveredFieldsCache[$core][$entity_type_id][$bundle][$row->getSourceProperty('field_name')] = $row->getSource();
}
}
return $this->discoveredFieldsCache[$core];
}
/**
* Gets all field information for a particular entity type.
*
* @param string $core
* The Drupal core version.
* @param string $entity_type_id
* The legacy entity type ID.
*
* @return array
* A multidimensional array of source data from the relevant field instance
* migration for the entity type, keyed first by bundle and then by field
* name.
*/
protected function getEntityFields($core, $entity_type_id) {
$fields = $this->getAllFields($core);
if (!empty($fields[$entity_type_id])) {
return $fields[$entity_type_id];
}
return [];
}
/**
* Gets all field information for a particular entity type and bundle.
*
* @param string $core
* The Drupal core version.
* @param string $entity_type_id
* The legacy entity type ID.
* @param string $bundle
* The legacy bundle (or content_type).
*
* @return array
* An array of source data from the relevant field instance migration for
* the bundle, keyed by field name.
*/
protected function getBundleFields($core, $entity_type_id, $bundle) {
$fields = $this->getEntityFields($core, $entity_type_id);
if (!empty($fields[$bundle])) {
return $fields[$bundle];
}
return [];
}
/**
* Gets the source plugin to use to gather field information.
*
* @param string $core
* The Drupal core version.
*
* @return array|\Drupal\migrate\Plugin\MigrateSourceInterface
* The source plugin, or an empty array if none can be found that meets
* requirements.
*/
protected function getSourcePlugin($core) {
$definition = $this->getFieldInstanceStubMigrationDefinition($core);
$source_plugin = $this->migrationPluginManager
->createStubMigration($definition)
->getSourcePlugin();
if ($source_plugin instanceof RequirementsInterface) {
try {
$source_plugin->checkRequirements();
}
catch (RequirementsException $e) {
// If checkRequirements() failed, the source database did not support
// fields (i.e., Field is not installed in D7). Therefore, $fields will
// be empty and below we'll return an empty array. The migration will
// proceed without adding fields.
$this->logger->notice('Field discovery failed for Drupal core version @core. Did this site have the Field module installed? Error: @message', [
'@core' => $core,
'@message' => $e->getMessage(),
]);
return [];
}
}
return $source_plugin;
}
/**
* Provides the stub migration definition for a given Drupal core version.
*
* @param string $core
* The Drupal core version.
*
* @return array
* The stub migration definition.
*/
protected function getFieldInstanceStubMigrationDefinition($core) {
return [
'destination' => ['plugin' => 'null'],
'idMap' => ['plugin' => 'null'],
'source' => [
'ignore_map' => TRUE,
'plugin' => $this->sourcePluginIds[$core],
],
];
}
/**
* Finds the core version of a Drupal migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
*
* @return string|bool
* A string representation of the Drupal version, or FALSE.
*
* @throws \InvalidArgumentException
*/
protected function getCoreVersion(MigrationInterface $migration) {
$tags = $migration->getMigrationTags();
if (in_array('Drupal 7', $tags, TRUE)) {
return FieldDiscoveryInterface::DRUPAL_7;
}
elseif (in_array('Drupal 6', $tags, TRUE)) {
return FieldDiscoveryInterface::DRUPAL_6;
}
throw new \InvalidArgumentException("Drupal Core version not found for this migration");
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Drupal\migrate_drupal;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Provides field discovery for Drupal 6 & 7 migrations.
*/
interface FieldDiscoveryInterface {
const DRUPAL_6 = '6';
const DRUPAL_7 = '7';
/**
* Adds the field processes to a migration.
*
* This method is used in field migrations to execute the migration process
* alter method specified by the 'field_plugin_method' key of the migration
* for all field plugins applicable to this Drupal to Drupal migration. This
* method is used internally for field, field instance, widget, and formatter
* migrations to allow field plugins to alter the process for these
* migrations.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to add process plugins to.
*
* @throws \InvalidArgumentException
*
* @internal
*/
public function addAllFieldProcesses(MigrationInterface $migration);
/**
* Adds the field processes for an entity to a migration.
*
* This method is used in field migrations to execute the migration process
* alter method specified by the 'field_plugin_method' key of the migration
* for all field plugins applicable to this Drupal to Drupal migration. This
* method is used internally for field, field instance, widget, and formatter
* migrations to allow field plugins to alter the process for these
* migrations.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to add processes to.
* @param string $entity_type_id
* The legacy entity type to add processes for.
*
* @throws \InvalidArgumentException
*/
public function addEntityFieldProcesses(MigrationInterface $migration, $entity_type_id);
/**
* Adds the field processes for a bundle to a migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to add processes to.
* @param string $entity_type_id
* The legacy entity type to add processes for.
* @param string $bundle
* The legacy bundle (or content_type) to add processes for.
*
* @throws \InvalidArgumentException
*/
public function addBundleFieldProcesses(MigrationInterface $migration, $entity_type_id, $bundle);
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate_drupal;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Alters container services.
*/
class MigrateDrupalServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
parent::alter($container);
$container->getDefinition('plugin.manager.migration')
->setClass(MigrationPluginManager::class)
->addArgument(new Reference('plugin.manager.migrate.source'))
->addArgument(new Reference('config.factory'));
}
}

View File

@@ -0,0 +1,270 @@
<?php
namespace Drupal\migrate_drupal;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\RequirementsInterface;
/**
* Configures the appropriate migrations for a given source Drupal database.
*/
trait MigrationConfigurationTrait {
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The migration plugin manager service.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The follow-up migration tags.
*
* @var string[]
*/
protected $followUpMigrationTags;
/**
* Gets the database connection for the source Drupal database.
*
* @param array $database
* Database array representing the source Drupal database.
*
* @return \Drupal\Core\Database\Connection
* The database connection for the source Drupal database.
*/
protected function getConnection(array $database) {
// Set up the connection.
Database::addConnectionInfo('upgrade', 'default', $database);
$connection = Database::getConnection('default', 'upgrade');
return $connection;
}
/**
* Gets the system data from the system table of the source Drupal database.
*
* @param \Drupal\Core\Database\Connection $connection
* Database connection to the source Drupal database.
*
* @return array
* The system data from the system table of the source Drupal database.
*/
protected function getSystemData(Connection $connection) {
$system_data = [];
try {
$results = $connection->select('system', 's', [
'fetch' => \PDO::FETCH_ASSOC,
])
->fields('s')
->execute();
foreach ($results as $result) {
$system_data[$result['type']][$result['name']] = $result;
}
}
catch (DatabaseExceptionWrapper $e) {
// The table might not exist for example in tests.
}
return $system_data;
}
/**
* Creates the necessary state entries for SqlBase::getDatabase() to work.
*
* The state entities created here have to exist before migration plugin
* instances are created so that derivers such as
* \Drupal\taxonomy\Plugin\migrate\D6TermNodeDeriver can access the source
* database.
*
* @param array $database
* The source database settings.
* @param string $drupal_version
* The Drupal version.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase::getDatabase()
*/
protected function createDatabaseStateSettings(array $database, $drupal_version) {
$database_state['key'] = 'upgrade';
$database_state['database'] = $database;
$database_state_key = 'migrate_drupal_' . $drupal_version;
$state = $this->getState();
$state->set($database_state_key, $database_state);
$state->set('migrate.fallback_state_key', $database_state_key);
}
/**
* Gets the migrations for import.
*
* @param string $database_state_key
* The state key.
* @param int $drupal_version
* The version of Drupal we're getting the migrations for.
*
* @return \Drupal\migrate\Plugin\MigrationInterface[]
* The migrations for import.
*/
protected function getMigrations($database_state_key, $drupal_version) {
$version_tag = 'Drupal ' . $drupal_version;
/** @var \Drupal\migrate\Plugin\MigrationInterface[] $all_migrations */
$all_migrations = $this->getMigrationPluginManager()->createInstancesByTag($version_tag);
// Unset the node migrations that should not run based on the type of node
// migration. That is, if this is a complete node migration then unset the
// classic node migrations and if this is a classic node migration then
// unset the complete node migrations.
$type = NodeMigrateType::getNodeMigrateType(\Drupal::database(), $drupal_version);
switch ($type) {
case NodeMigrateType::NODE_MIGRATE_TYPE_COMPLETE:
$patterns = '/(d' . $drupal_version . '_node:)|(d' . $drupal_version . '_node_translation:)|(d' . $drupal_version . '_node_revision:)|(d7_node_entity_translation:)/';
break;
case NodeMigrateType::NODE_MIGRATE_TYPE_CLASSIC:
$patterns = '/(d' . $drupal_version . '_node_complete:)/';
break;
}
foreach ($all_migrations as $key => $migrations) {
if (preg_match($patterns, $key)) {
unset($all_migrations[$key]);
}
}
$migrations = [];
foreach ($all_migrations as $migration) {
// Skip migrations tagged with any of the follow-up migration tags. They
// will be derived and executed after the migrations on which they depend
// have been successfully executed.
// @see Drupal\migrate_drupal\Plugin\MigrationWithFollowUpInterface
if (!empty(array_intersect($migration->getMigrationTags(), $this->getFollowUpMigrationTags()))) {
continue;
}
try {
// @todo https://drupal.org/node/2681867 We should be able to validate
// the entire migration at this point.
$source_plugin = $migration->getSourcePlugin();
if ($source_plugin instanceof RequirementsInterface) {
$source_plugin->checkRequirements();
}
$destination_plugin = $migration->getDestinationPlugin();
if ($destination_plugin instanceof RequirementsInterface) {
$destination_plugin->checkRequirements();
}
$migrations[] = $migration;
}
catch (RequirementsException $e) {
// Migrations which are not applicable given the source and destination
// site configurations (e.g., what modules are enabled) will be silently
// ignored.
}
}
return $migrations;
}
/**
* Returns the follow-up migration tags.
*
* @return string[]
*/
protected function getFollowUpMigrationTags() {
if ($this->followUpMigrationTags === NULL) {
$this->followUpMigrationTags = $this->getConfigFactory()
->get('migrate_drupal.settings')
->get('follow_up_migration_tags') ?: [];
}
return $this->followUpMigrationTags;
}
/**
* Determines what version of Drupal the source database contains.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection object.
*
* @return string|false
* A string representing the major branch of Drupal core (e.g. '6' for
* Drupal 6.x), or FALSE if no valid version is matched.
*/
public static function getLegacyDrupalVersion(Connection $connection) {
// Don't assume because a table of that name exists, that it has the columns
// we're querying. Catch exceptions and report that the source database is
// not Drupal.
// Drupal 5/6/7 can be detected by the schema_version in the system table.
if ($connection->schema()->tableExists('system')) {
try {
$version_string = $connection
->query('SELECT [schema_version] FROM {system} WHERE [name] = :module', [':module' => 'system'])
->fetchField();
}
catch (DatabaseExceptionWrapper $e) {
// All database errors return FALSE.
}
}
return match (TRUE) {
!isset($version_string) => FALSE,
(int) $version_string >= 6000 => substr($version_string, 0, 1),
(int) $version_string >= 1000 => '5',
default => FALSE,
};
}
/**
* Gets the config factory service.
*
* @return \Drupal\Core\Config\ConfigFactoryInterface
* The config factory service.
*/
protected function getConfigFactory() {
if (!$this->configFactory) {
$this->configFactory = \Drupal::service('config.factory');
}
return $this->configFactory;
}
/**
* Gets the migration plugin manager service.
*
* @return \Drupal\migrate\Plugin\MigrationPluginManagerInterface
* The migration plugin manager service.
*/
protected function getMigrationPluginManager() {
if (!$this->migrationPluginManager) {
$this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
}
return $this->migrationPluginManager;
}
/**
* Gets the state service.
*
* @return \Drupal\Core\State\StateInterface
* The state service.
*/
protected function getState() {
if (!$this->state) {
$this->state = \Drupal::service('state');
}
return $this->state;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\migrate_drupal;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\migrate\Plugin\Exception\BadPluginDefinitionException;
use Drupal\migrate\Plugin\MigrateSourcePluginManager;
use Drupal\migrate\Plugin\MigrationPluginManager as BaseMigrationPluginManager;
/**
* Manages migration plugins.
*
* Analyzes migration definitions to ensure that the source plugin of any
* migration tagged with particular tags ('Drupal 6' or 'Drupal 7' by default)
* defines a source_module property in its plugin annotation. This is done in
* order to support the Migrate Drupal UI, which needs to know which modules
* "own" the data being migrated into Drupal 8, on both the source and
* destination sides.
*
* @todo Enforce the destination_module property too, in
* https://www.drupal.org/project/drupal/issues/2923810.
*/
class MigrationPluginManager extends BaseMigrationPluginManager {
/**
* The Migrate source plugin manager service.
*
* @var \Drupal\migrate\Plugin\MigrateSourcePluginManager
*/
protected $sourceManager;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The migration tags which will trigger source_module enforcement.
*
* @var string[]
*/
protected $enforcedSourceModuleTags;
/**
* MigrationPluginManager constructor.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* @param \Drupal\migrate\Plugin\MigrateSourcePluginManager $source_manager
* The Migrate source plugin manager service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
*/
public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager, MigrateSourcePluginManager $source_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($module_handler, $cache_backend, $language_manager);
$this->sourceManager = $source_manager;
$this->configFactory = $config_factory;
}
/**
* Returns the migration tags that trigger source_module enforcement.
*
* @return string[]
*/
protected function getEnforcedSourceModuleTags() {
if ($this->enforcedSourceModuleTags === NULL) {
$this->enforcedSourceModuleTags = $this->configFactory
->get('migrate_drupal.settings')
->get('enforce_source_module_tags') ?: [];
}
return $this->enforcedSourceModuleTags;
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
// If the migration has no tags, we don't need to enforce the source_module
// annotation property.
if (empty($definition['migration_tags'])) {
return;
}
// Check if the migration has any of the tags that trigger source_module
// enforcement.
$applied_tags = array_intersect($this->getEnforcedSourceModuleTags(), $definition['migration_tags']);
if ($applied_tags) {
// Throw an exception if the source plugin definition does not define a
// source_module.
$source_id = $definition['source']['plugin'];
$source_definition = $this->sourceManager->getDefinition($source_id);
if (empty($source_definition['source_module'])) {
throw new BadPluginDefinitionException($source_id, 'source_module');
}
}
}
}

View File

@@ -0,0 +1,457 @@
<?php
namespace Drupal\migrate_drupal;
use Drupal\Core\Discovery\YamlDiscovery;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
/**
* Determines the migrate state for all modules enabled on the source.
*
* Retrieves migrate info from *.migrate_drupal.yml files.
*
* Knowing which modules will be upgraded and those that will not is needed by
* anyone upgrading a legacy Drupal version. This service provides that
* information by analyzing the existing migrations and data in
* migrate_drupal.yml files. Modules that are enabled or disabled in the source
* are included in the analysis modules that are uninstalled are ignored.
*
* Deciding the upgrade state of a source module is a complicated task. A
* destination module is not limited in any way to the source modules or the
* current major version destination modules it is providing migrations for. We
* see this in core where the Drupal 6 Menu module is upgraded by having
* migrations in three Drupal 8 modules; menu_link_content, menu_ui and system.
* If migrations for any of those three modules are not complete or if any of
* them are not installed on the destination site then the Drupal 6 Menu module
* cannot be listed as upgraded. If any one of the conditions are not met then
* it should be listed as will not be upgraded.
*
* Another challenge is to ensure that legacy source modules that do not need an
* upgrade path are handled correctly. These will not have migrations but should
* be listed as will be upgraded, which even though there are not migrations
* under the hood, it lets a site admin know that upgrading with this module
* enabled is safe.
*
* There is not enough information in the existing system to determine the
* correct state of the upgrade path for these, and other scenarios.
*
* The solution is for every destination module that is the successor to a
* module built for a legacy Drupal version to declare the state of the upgrade
* path(s) for the module. A module's upgrade path from a previous version may
* consist of one or more migrations sets. Each migration set definition
* consists of a source module supporting a legacy Drupal version, and one or
* more current destination modules. This allows a module to indicate that a
* provided migration set requires additional modules to be enabled in the
* destination.
*
* A migration set can be marked 'finished', which indicates that all
* migrations that are going to be provided by this destination module for this
* migration set have been written and are complete. A migration set may also
* be marked 'not_finished' which indicates that the module either has not
* provided any migrations for the set, or needs to provide additional
* migrations to complete the set. Note that other modules may still provide
* additional finished or not_finished migrations for the same migration set.
*
* Modules inform the upgrade process of the migration sets by adding them to
* their <module_name>.migrate_drupal.yml file.
*
* The <module_name>.migrate_drupal.yml file uses the following structure:
*
* # (optional) List of the source_module/destination_module(s) for the
* # migration sets that this module provides and are complete.
* finished:
* # One or more Drupal legacy version number mappings (i.e. 6 and/or 7).
* 6:
* # A mapping of legacy module machine names to either an array of modules
* # or a single destination module machine name to define this migration
* # set.
* <source_module_1>: <destination_module_1>
* <source_module_2>:
* - <destination_module_1>
* - <destination_module_2>
* 7:
* <source_module_1>: <destination_module_1>
* <source_module_2>:
* - <destination_module_1>
* - <destination_module_2>
* # (optional) List of the migration sets that this module provides, or will be
* # providing, that are incomplete or do not yet exist.
* not_finished:
* 6:
* <source_module_1>: <destination_module_1>
* <source_module_2>:
* - <destination_module_1>
* - <destination_module_2>
*
* Examples:
*
* @code
* finished:
* 6:
* node: node
* 7:
* node: node
* entity_translation: node
* not_finished:
* 7:
* commerce_product: commerce_product
* other_module:
* - other_module
* - further_module
* @endcode
*
* In this example the module has completed the upgrade path for data from the
* Drupal 6 and Drupal 7 Node modules to the Drupal 8 Node module and for data
* from the Drupal 7 Entity Translation module to the Drupal 8 Node module.
*
* @code
* finished:
* 6:
* pirate: pirate
* 7:
* pirate: pirate
* @endcode
*
* The Pirate module does not require an upgrade path. By declaring the upgrade
* finished the Pirate module will be included in the finished list. That is,
* as long as no other module has an entry "pirate: <any module name>' in its
* not_finished section.
*/
class MigrationState {
use MessengerTrait;
use StringTranslationTrait;
/**
* Source module upgrade state when all its migrations are complete.
*
* @var string
*/
const FINISHED = 'finished';
/**
* Source module upgrade state when all its migrations are not complete.
*
* @var string
*/
const NOT_FINISHED = 'not_finished';
/**
* The field plugin manager service.
*
* @var \Drupal\Core\Extension\ModuleHandler
*/
protected $moduleHandler;
/**
* The field plugin manager service.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
*/
protected $fieldPluginManager;
/**
* An array of migration states declared for each source migration.
*
* States are keyed by version. Each value is an array keyed by name of the
* source module and the value is an array of all the states declared for this
* source module.
*
* @var array
*/
protected $stateBySource;
/**
* An array of destinations declared for each source migration.
*
* Destinations are keyed by version. Each value is an array keyed by the name
* of the source module and the value is an array of the destination modules.
*
* @var array
*/
protected $declaredBySource;
/**
* An array of migration source and destinations derived from migrations.
*
* The key is the source version and the value is an array where the key is
* the source module and the value is an array of destinations derived from
* migration plugins.
*
* @var array
*/
protected $discoveredBySource;
/**
* An array of migration source and destinations.
*
* Values are derived from migration plugins and declared states. The key is
* the source version and the value is an array where the key is the source
* module and the value is an array of declared or derived destinations.
*
* @var array
*/
protected $destinations = [];
/**
* Array of enabled modules.
*
* @var array
*/
protected $enabledModules = [];
/**
* Construct a new MigrationState object.
*
* @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $fieldPluginManager
* Field plugin manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* Module handler.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* Messenger service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
* String translation service.
*/
public function __construct(MigrateFieldPluginManagerInterface $fieldPluginManager, ModuleHandlerInterface $moduleHandler, MessengerInterface $messenger, TranslationInterface $stringTranslation) {
$this->fieldPluginManager = $fieldPluginManager;
$this->moduleHandler = $moduleHandler;
$this->enabledModules = array_keys($this->moduleHandler->getModuleList());
$this->enabledModules[] = 'core';
$this->messenger = $messenger;
$this->stringTranslation = $stringTranslation;
}
/**
* Gets the upgrade states for all enabled source modules.
*
* @param string $version
* The legacy drupal version.
* @param array $source_system_data
* The data from the source site system table.
* @param array $migrations
* An array of migrations.
*
* @return array
* An associative array of data with keys of state, source modules and a
* value which is a comma separated list of destination modules.
*/
public function getUpgradeStates($version, array $source_system_data, array $migrations) {
return $this->buildUpgradeState($version, $source_system_data, $migrations);
}
/**
* Gets migration state information from *.migrate_drupal.yml.
*
* @return array
* An association array keyed by module of the finished and not_finished
* migrations for each module.
* */
protected function getMigrationStates() {
// Always instantiate a new YamlDiscovery object so that we always search on
// the up-to-date list of modules.
$discovery = new YamlDiscovery('migrate_drupal', array_map(function ($value) {
return $value . '/migrations/state';
}, $this->moduleHandler->getModuleDirectories()));
return $discovery->findAll();
}
/**
* Determines migration state for each source module enabled on the source.
*
* If there are no migrations for a module and no declared state the state is
* set to NOT_FINISHED. When a module does not need any migrations, such as
* Overlay, a state of finished is declared in system.migrate_drupal.yml.
*
* If there are migrations for a module the following happens. If the
* destination module is 'core' the state is set to FINISHED. If there are
* any occurrences of 'not_finished' in the *.migrate_drupal.yml information
* for this source module then the state is set to NOT_FINISHED. And finally,
* if there is an occurrence of 'finished' the state is set to FINISHED.
*
* @param string $version
* The legacy drupal version.
* @param array $source_system_data
* The data from the source site system table.
* @param array $migrations
* An array of migrations.
*
* @return array
* An associative array of data with keys of state, source modules and a
* value which is a comma separated list of destination modules.
* Example.
*
* @code
* [
* 'finished' => [
* 'menu' => [
* 'menu_link_content','menu_ui','system'
* ]
* ],
* ]
* @endcode
*/
protected function buildUpgradeState($version, array $source_system_data, array $migrations) {
// Remove core profiles from the system data.
unset($source_system_data['module']['standard'], $source_system_data['module']['minimal']);
$this->buildDiscoveredDestinationsBySource($version, $migrations, $source_system_data);
$this->buildDeclaredStateBySource($version);
$upgrade_state = [];
// Loop through every source module that is enabled on the source site.
foreach ($source_system_data['module'] as $module) {
// The source plugins check requirements requires that all
// source_modules are enabled so do the same here.
if ($module['status']) {
$source_module = $module['name'];
$upgrade_state[$this->getSourceState($version, $source_module)][$source_module] = implode(', ', $this->getDestinationsForSource($version, $source_module));
}
}
foreach ($upgrade_state as $key => $value) {
ksort($upgrade_state[$key]);
}
return $upgrade_state;
}
/**
* Builds migration source and destination module information.
*
* @param string $version
* The legacy Drupal version.
* @param array $migrations
* The discovered migrations.
* @param array $source_system_data
* The data from the source site system table.
*/
protected function buildDiscoveredDestinationsBySource($version, array $migrations, array $source_system_data) {
$discovered_upgrade_paths = [];
$table_data = [];
foreach ($migrations as $migration) {
$migration_id = $migration->getPluginId();
$source_module = $migration->getSourcePlugin()->getSourceModule();
if (!$source_module) {
$this->messenger()
->addError($this->t('Source module not found for @migration_id.', ['@migration_id' => $migration_id]));
}
$destination_module = $migration->getDestinationPlugin()
->getDestinationModule();
if (!$destination_module) {
$this->messenger()
->addError($this->t('Destination module not found for @migration_id.', ['@migration_id' => $migration_id]));
}
if ($source_module && $destination_module) {
$discovered_upgrade_paths[$source_module][] = $destination_module;
$table_data[$source_module][$destination_module][$migration_id] = $migration->label();
}
}
// Add entries for the field plugins to discovered_upgrade_paths.
$definitions = $this->fieldPluginManager->getDefinitions();
foreach ($definitions as $definition) {
// This is not strict so that we find field plugins with an annotation
// where the Drupal core version is an integer and when it is a string.
if (in_array($version, $definition['core'])) {
$source_module = $definition['source_module'];
$destination_module = $definition['destination_module'];
$discovered_upgrade_paths[$source_module][] = $destination_module;
$table_data[$source_module][$destination_module][$definition['id']] = $definition['id'];
}
}
ksort($table_data);
foreach ($table_data as $source_module => $destination_module_info) {
ksort($table_data[$source_module]);
}
$this->discoveredBySource[$version] = array_map('array_unique', $discovered_upgrade_paths);
}
/**
* Gets migration data from *.migrate_drupal.yml sorted by source module.
*
* @param string $version
* The legacy Drupal version.
*/
protected function buildDeclaredStateBySource($version) {
$migration_states = $this->getMigrationStates();
$state_by_source = [];
$dest_by_source = [];
$states = [static::FINISHED, static::NOT_FINISHED];
foreach ($migration_states as $info) {
foreach ($states as $state) {
if (isset($info[$state][$version])) {
foreach ($info[$state][$version] as $source => $destination) {
// Add the state.
$state_by_source[$source][] = $state;
// Add the destination modules.
$dest_by_source += [$source => []];
$dest_by_source[$source] = array_merge($dest_by_source[$source], (array) $destination);
}
}
}
}
$this->stateBySource[$version] = array_map('array_unique', $state_by_source);
$this->declaredBySource[$version] = array_map('array_unique', $dest_by_source);
}
/**
* Tests if a destination exists for the given source module.
*
* @param string $version
* Source version of Drupal.
* @param string $source_module
* Source module.
*
* @return string
* Migration state, either 'finished' or 'not_finished'.
*/
protected function getSourceState($version, $source_module) {
// The state is finished only when no declarations of 'not_finished'
// were found and each destination module is enabled.
if (!$destinations = $this->getDestinationsForSource($version, $source_module)) {
// No discovered or declared state.
return MigrationState::NOT_FINISHED;
}
if (!isset($this->stateBySource[$version][$source_module])) {
// No declared state.
return MigrationState::NOT_FINISHED;
}
if (in_array(MigrationState::NOT_FINISHED, $this->stateBySource[$version][$source_module], TRUE) || !in_array(MigrationState::FINISHED, $this->stateBySource[$version][$source_module], TRUE)) {
return MigrationState::NOT_FINISHED;
}
if (array_diff($destinations, $this->enabledModules)) {
return MigrationState::NOT_FINISHED;
}
return MigrationState::FINISHED;
}
/**
* Get net destinations for source module.
*
* @param string $version
* Source version.
* @param string $source_module
* Source module.
*
* @return array
* Destination modules either declared by {modulename}.migrate_drupal.yml
* files or discovered from migration plugins.
*/
protected function getDestinationsForSource($version, $source_module) {
if (!isset($this->destinations[$version][$source_module])) {
$this->discoveredBySource[$version] += [$source_module => []];
$this->declaredBySource[$version] += [$source_module => []];
$destination = array_unique(array_merge($this->discoveredBySource[$version][$source_module], $this->declaredBySource[$version][$source_module]));
sort($destination);
$this->destinations[$version][$source_module] = $destination;
}
return $this->destinations[$version][$source_module];
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\migrate_drupal;
use Drupal\Core\Database\Connection;
use Drupal\Core\Site\Settings;
/**
* Provides a class to determine the type of migration.
*/
final class NodeMigrateType {
use MigrationConfigurationTrait;
/**
* Only the complete node migration map tables are in use.
*/
const NODE_MIGRATE_TYPE_COMPLETE = 'COMPLETE';
/**
* Only the classic node migration map tables are in use.
*/
const NODE_MIGRATE_TYPE_CLASSIC = 'CLASSIC';
/**
* Determines the type of node migration to be used.
*
* The node complete migration is the default. It is not used when there
* are existing tables for dN_node.
*
* @param \Drupal\Core\Database\Connection $connection
* The connection to the target database.
* @param string|false $version
* The Drupal version of the source database, FALSE if it cannot be
* determined.
*
* @return string
* The migrate type.
*
* @internal
*/
public static function getNodeMigrateType(Connection $connection, $version) {
$migrate_node_migrate_type_classic = Settings::get('migrate_node_migrate_type_classic', FALSE);
if ($migrate_node_migrate_type_classic) {
return static::NODE_MIGRATE_TYPE_CLASSIC;
}
$migrate_type = static::NODE_MIGRATE_TYPE_COMPLETE;
if ($version) {
// Create the variable name, 'node_has_rows' or 'node_complete_exists' and
// set it the default value, FALSE.
$node_has_rows = FALSE;
$node_complete_has_rows = FALSE;
// Find out what migrate map tables have rows for the node migrations.
// It is either the classic, 'dN_node', or the complete,
// 'dN_node_complete', or both. This is used to determine which migrations
// are run and if migrations using the node migrations in a
// migration_lookup are altered.
$bases = ['node', 'node_complete'];
$tables = $connection->schema()
->findTables('migrate_map_d' . $version . '_node%');
foreach ($bases as $base) {
$has_rows = $base . '_has_rows';
$base_tables = preg_grep('/^migrate_map_d' . $version . '_' . $base . '_{2}.*$/', $tables);
// Set the has_rows True when a map table has rows with a positive
// count for the matched migration.
foreach ($base_tables as $base_table) {
if ($connection->schema()->tableExists($base_table)) {
$count = $connection->select($base_table)->countQuery()
->execute()->fetchField();
if ($count > 0) {
$$has_rows = TRUE;
break;
}
}
}
}
// Set the node migration type to use.
if ($node_has_rows && !$node_complete_has_rows) {
$migrate_type = static::NODE_MIGRATE_TYPE_CLASSIC;
}
}
return $migrate_type;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\migrate_drupal\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Provides an interface for all field type plugins.
*/
interface MigrateFieldInterface extends PluginInspectionInterface {
/**
* Apply any custom processing to the field migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity.
*/
public function alterFieldMigration(MigrationInterface $migration);
/**
* Apply any custom processing to the field instance migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity.
*/
public function alterFieldInstanceMigration(MigrationInterface $migration);
/**
* Apply any custom processing to the field widget migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity.
*/
public function alterFieldWidgetMigration(MigrationInterface $migration);
/**
* Apply any custom processing to the field formatter migration.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity.
*/
public function alterFieldFormatterMigration(MigrationInterface $migration);
/**
* Get the field formatter type from the source.
*
* @param \Drupal\migrate\Row $row
* The field being migrated.
*
* @return string
* The field formatter type.
*/
public function getFieldFormatterType(Row $row);
/**
* Get a map between D6 formatters and D8 formatters for this field type.
*
* This is used by static::alterFieldFormatterMigration() in the base class.
*
* @return array
* The keys are D6 formatters and the values are D8 formatters.
*/
public function getFieldFormatterMap();
/**
* Get the field widget type from the source.
*
* @param \Drupal\migrate\Row $row
* The field being migrated.
*
* @return string
* The field widget type.
*/
public function getFieldWidgetType(Row $row);
/**
* Get a map between D6 and D8 widgets for this field type.
*
* @return array
* The keys are D6 field widget types and the values D8 widgets.
*/
public function getFieldWidgetMap();
/**
* Apply any custom processing to the field bundle migrations.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity.
* @param string $field_name
* The field name we're processing the value for.
* @param array $data
* The array of field data from FieldValues::fieldData().
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data);
/**
* Computes the destination type of a migrated field.
*
* @param \Drupal\migrate\Row $row
* The field being migrated.
*
* @return string
* The destination field type.
*/
public function getFieldType(Row $row);
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Drupal\migrate_drupal\Plugin;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\migrate\Plugin\Exception\BadPluginDefinitionException;
use Drupal\migrate\Plugin\MigratePluginManager;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Plugin manager for migrate field plugins.
*
* @see \Drupal\migrate_drupal\Plugin\MigrateFieldInterface
* @see \Drupal\migrate\Attribute\MigrateField
* @see plugin_api
*
* @ingroup migration
*/
class MigrateFieldPluginManager extends MigratePluginManager implements MigrateFieldPluginManagerInterface {
/**
* The default version of core to use for field plugins.
*
* These plugins were initially only built and used for Drupal 6 fields.
* Having been extended for Drupal 7 with a "core" annotation, we fall back to
* Drupal 6 where none exists.
*/
const DEFAULT_CORE_VERSION = 6;
/**
* Get the plugin ID from the field type.
*
* This method determines which field plugin should be used for a given field
* type and Drupal core version, returning the lowest weighted plugin
* supporting the provided core version, and which matches the field type
* either by plugin ID, or in the type_map annotation keys.
*
* @param string $field_type
* The field type being migrated.
* @param array $configuration
* (optional) An array of configuration relevant to the plugin instance.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* (optional) The current migration instance.
*
* @return string
* The ID of the plugin for the field type if available.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* If the plugin cannot be determined, such as if the field type is invalid.
*
* @see \Drupal\migrate_drupal\Attribute\MigrateField
*/
public function getPluginIdFromFieldType($field_type, array $configuration = [], ?MigrationInterface $migration = NULL) {
$core = static::DEFAULT_CORE_VERSION;
if (!empty($configuration['core'])) {
$core = $configuration['core'];
}
elseif (!empty($migration->getPluginDefinition()['migration_tags'])) {
foreach ($migration->getPluginDefinition()['migration_tags'] as $tag) {
if ($tag == 'Drupal 7') {
$core = 7;
}
}
}
$definitions = $this->getDefinitions();
foreach ($definitions as $plugin_id => $definition) {
if (in_array($core, $definition['core'])) {
if (array_key_exists($field_type, $definition['type_map']) || $field_type === $plugin_id) {
return $plugin_id;
}
}
}
throw new PluginNotFoundException($field_type);
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
foreach (['core', 'source_module', 'destination_module'] as $required_property) {
if (empty($definition[$required_property])) {
throw new BadPluginDefinitionException($plugin_id, $required_property);
}
}
}
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = parent::findDefinitions();
$this->sortDefinitions($definitions);
return $definitions;
}
/**
* Sorts a definitions array.
*
* This sorts the definitions array first by the weight column, and then by
* the plugin ID, ensuring a stable, deterministic, and testable ordering of
* plugins.
*
* @param array $definitions
* The definitions array to sort.
*/
protected function sortDefinitions(array &$definitions) {
array_multisort(array_column($definitions, 'weight'), SORT_ASC, SORT_NUMERIC, array_keys($definitions), SORT_ASC, SORT_NATURAL, $definitions);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\migrate_drupal\Plugin;
use Drupal\migrate\Plugin\MigratePluginManagerInterface;
use Drupal\migrate\Plugin\MigrationInterface;
interface MigrateFieldPluginManagerInterface extends MigratePluginManagerInterface {
/**
* Get the plugin ID from the field type.
*
* @param string $field_type
* The field type being migrated.
* @param array $configuration
* (optional) An array of configuration relevant to the plugin instance.
* @param \Drupal\migrate\Plugin\MigrationInterface|null $migration
* (optional) The current migration instance.
*
* @return string
* The ID of the plugin for the field_type if available.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* If the plugin cannot be determined, such as if the field type is invalid.
*/
public function getPluginIdFromFieldType($field_type, array $configuration = [], ?MigrationInterface $migration = NULL);
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\migrate_drupal\Plugin;
/**
* Interface for migrations with follow-up migrations.
*
* Some migrations need to be derived and executed after other migrations have
* been successfully executed. For example, a migration might need to be derived
* based on previously migrated data. For such a case, the migration dependency
* system is not enough since all migrations would still be derived before any
* one of them has been executed.
*
* Those "follow-up" migrations need to be tagged with the "Follow-up migration"
* tag (or any tag in the "follow_up_migration_tags" configuration) and thus
* they won't be derived with the other migrations.
*
* To get those follow-up migrations derived at the right time, the migrations
* on which they depend must implement this interface and generate them in the
* generateFollowUpMigrations() method.
*
* When the migrations implementing this interface have been successfully
* executed, the follow-up migrations will then be derived having access to the
* now migrated data.
*/
interface MigrationWithFollowUpInterface {
/**
* Generates follow-up migrations.
*
* When the migration implementing this interface has been successfully
* executed, this method will be used to generate the follow-up migrations
* which depends on the now migrated data.
*
* @return \Drupal\migrate\Plugin\MigrationInterface[]
* The follow-up migrations.
*/
public function generateFollowUpMigrations();
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for entity reference field translations.
*
* A migration will be created for every bundle with at least one entity
* reference field that is configured to point to one of the supported target
* entity types. The migrations will update the entity reference fields with
* values found in the mapping tables of the migrations associated with the
* target types.
*
* Example:
*
* @code
* id: d7_entity_reference_translation
* label: Entity reference translations
* migration_tags:
* - Drupal 7
* - Follow-up migration
* deriver: Drupal\migrate_drupal\Plugin\migrate\EntityReferenceTranslationDeriver
* target_types:
* node:
* - d7_node_translation
* source:
* plugin: empty
* key: default
* target: default
* process: []
* destination:
* plugin: null
* @endcode
*
* In this example, the only supported target type is 'node' and the associated
* migration for the mapping table lookup is 'd7_node_translation'.
*/
class EntityReferenceTranslationDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* EntityReferenceTranslationDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct($base_plugin_id, EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager) {
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('entity_field.manager'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
// Get all entity reference fields.
$field_map = $this->entityFieldManager->getFieldMapByFieldType('entity_reference');
foreach ($field_map as $entity_type => $fields) {
foreach ($fields as $field_name => $field) {
foreach ($field['bundles'] as $bundle) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle);
$target_type = $field_definitions[$field_name]->getSetting('target_type');
// If the field's target type is not supported, skip it.
if (!array_key_exists($target_type, $base_plugin_definition['target_types'])) {
continue;
}
// Key derivatives by entity types and bundles.
$derivative_key = $entity_type . '__' . $bundle;
$derivative = $base_plugin_definition;
$entity_type_definition = $this->entityTypeManager->getDefinition($entity_type);
// Set the migration label.
$derivative['label'] = $this->t('@label (@derivative)', [
'@label' => $base_plugin_definition['label'],
'@derivative' => $derivative_key,
]);
// Set the source plugin.
$derivative['source']['plugin'] = 'content_entity' . PluginBase::DERIVATIVE_SEPARATOR . $entity_type;
if ($entity_type_definition->hasKey('bundle')) {
$derivative['source']['bundle'] = $bundle;
}
// Set the process pipeline.
$id_key = $entity_type_definition->getKey('id');
$derivative['process'][$id_key] = $id_key;
if ($entity_type_definition->isRevisionable()) {
$revision_key = $entity_type_definition->getKey('revision');
$derivative['process'][$revision_key] = $revision_key;
}
if ($entity_type_definition->isTranslatable()) {
$langcode_key = $entity_type_definition->getKey('langcode');
$derivative['process'][$langcode_key] = $langcode_key;
}
// Set the destination plugin.
$derivative['destination']['plugin'] = 'entity' . PluginBase::DERIVATIVE_SEPARATOR . $entity_type;
if ($entity_type_definition->hasKey('bundle')) {
$derivative['destination']['default_bundle'] = $bundle;
}
if ($entity_type_definition->isTranslatable()) {
$derivative['destination']['translations'] = TRUE;
}
// Allow overwriting the entity reference field so we can update its
// values with the ones found in the mapping table.
$derivative['destination']['overwrite_properties'][$field_name] = $field_name;
// Add the entity reference field to the process pipeline.
$derivative['process'][$field_name] = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'translation_target_id' => [
[
'plugin' => 'migration_lookup',
'source' => 'target_id',
'migration' => $base_plugin_definition['target_types'][$target_type],
'no_stub' => TRUE,
],
[
'plugin' => 'skip_on_empty',
'method' => 'process',
],
[
'plugin' => 'extract',
'index' => [0],
],
],
'target_id' => [
[
'plugin' => 'null_coalesce',
'source' => [
'@translation_target_id',
'target_id',
],
],
],
],
];
if (!isset($this->derivatives[$derivative_key])) {
// If this is a new derivative, add it to the returned derivatives.
$this->derivatives[$derivative_key] = $derivative;
}
else {
// If this is an existing derivative, it means this bundle has more
// than one entity reference field. In that case, we only want to
// add the field to the process pipeline and add it to
// overwrite_properties so it can be overwritten.
$this->derivatives[$derivative_key]['process'] += $derivative['process'];
$this->derivatives[$derivative_key]['destination']['overwrite_properties'] += $derivative['destination']['overwrite_properties'];
}
}
}
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\MigrateDestinationPluginManager;
use Drupal\migrate\Plugin\MigratePluginManager;
use Drupal\migrate\Plugin\Migration;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Migration plugin class for migrations dealing with field config and values.
*/
class FieldMigration extends Migration implements ContainerFactoryPluginInterface {
/**
* Flag indicating whether the field data has been filled already.
*
* @var bool
*/
protected $init = FALSE;
/**
* The migration field discovery service.
*
* @var \Drupal\migrate_drupal\FieldDiscoveryInterface
*/
protected $fieldDiscovery;
/**
* Constructs a FieldMigration.
*
* @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\MigratePluginManager $source_plugin_manager
* The source migration plugin manager.
* @param \Drupal\migrate\Plugin\MigratePluginManager $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\MigratePluginManager $id_map_plugin_manager
* The ID map migration plugin manager.
* @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
* The migration field discovery service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManager $source_plugin_manager, MigratePluginManager $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManager $id_map_plugin_manager, FieldDiscoveryInterface $field_discovery) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration_plugin_manager, $source_plugin_manager, $process_plugin_manager, $destination_plugin_manager, $id_map_plugin_manager);
$this->fieldDiscovery = $field_discovery;
}
/**
* {@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'),
$container->get('migrate_drupal.field_discovery')
);
}
/**
* {@inheritdoc}
*/
public function getProcess() {
if (!$this->init) {
$this->init = TRUE;
$this->fieldDiscovery->addAllFieldProcesses($this);
}
return parent::getProcess();
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\field;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\MigrateFieldInterface;
/**
* The base class for all field plugins.
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate_drupal\Attribute\MigrateField
* @see \Drupal\migrate_drupal\Plugin\MigrateFieldInterface
* @see plugin_api
*
* @ingroup migration
*/
abstract class FieldPluginBase extends PluginBase implements MigrateFieldInterface {
/**
* {@inheritdoc}
*/
public function alterFieldMigration(MigrationInterface $migration) {
$process[0]['map'][$this->pluginId][$this->pluginId] = $this->pluginId;
$migration->mergeProcessOfProperty('type', $process);
}
/**
* {@inheritdoc}
*/
public function alterFieldInstanceMigration(MigrationInterface $migration) {
// Nothing to do by default with field instances.
}
/**
* {@inheritdoc}
*/
public function alterFieldWidgetMigration(MigrationInterface $migration) {
$process = [];
foreach ($this->getFieldWidgetMap() as $source_widget => $destination_widget) {
$process['type']['map'][$source_widget] = $destination_widget;
}
$migration->mergeProcessOfProperty('options/type', $process);
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterType(Row $row) {
return $row->getSourceProperty('formatter/type');
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [];
}
/**
* {@inheritdoc}
*/
public function getFieldWidgetType(Row $row) {
return $row->getSourceProperty('widget/type');
}
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
// By default, use the plugin ID for the widget types.
return [
$this->pluginId => $this->pluginId . '_default',
];
}
/**
* {@inheritdoc}
*/
public function alterFieldFormatterMigration(MigrationInterface $migration) {
$process = [];
// Some migrate field plugin IDs are prefixed with 'd6_' or 'd7_'. Since the
// plugin ID is used in the static map as the module name, we have to remove
// this prefix from the plugin ID.
$plugin_id = preg_replace('/d[67]_/', '', $this->pluginId);
foreach ($this->getFieldFormatterMap() as $source_format => $destination_format) {
$process[0]['map'][$plugin_id][$source_format] = $destination_format;
}
$migration->mergeProcessOfProperty('options/type', $process);
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'get',
'source' => $field_name,
];
$migration->mergeProcessOfProperty($field_name, $process);
}
/**
* {@inheritdoc}
*/
public function getFieldType(Row $row) {
$field_type = $row->getSourceProperty('type');
if (isset($this->pluginDefinition['type_map'][$field_type])) {
return $this->pluginDefinition['type_map'][$field_type];
}
else {
return $field_type;
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\field;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Base class for Drupal reference fields.
*/
abstract class ReferenceBase extends FieldPluginBase {
/**
* Gets the plugin ID for the reference type migration.
*
* The reference type migration will be added as a required dependency.
*
* @return string
* The plugin id.
*/
abstract protected function getEntityTypeMigrationId();
/**
* Gets the name of the field property which holds the entity ID.
*
* @return string
* The entity id.
*/
abstract protected function entityId();
/**
* {@inheritdoc}
*/
public function alterFieldInstanceMigration(MigrationInterface $migration) {
parent::alterFieldInstanceMigration($migration);
// Add the reference migration as a required dependency to this migration.
$migration_dependencies = $migration->getMigrationDependencies(TRUE);
array_push($migration_dependencies['required'], $this->getEntityTypeMigrationId());
$migration_dependencies['required'] = array_unique($migration_dependencies['required']);
$migration->set('migration_dependencies', $migration_dependencies);
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => ['target_id' => $this->entityId()],
];
$migration->setProcessOfProperty($field_name, $process);
}
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
return [
$this->pluginId . '_select' => 'options_select',
$this->pluginId . '_buttons' => 'options_buttons',
$this->pluginId . '_autocomplete' => 'entity_reference_autocomplete_tags',
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\field\d6;
// cspell:ignore nodereference
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\ReferenceBase;
/**
* MigrateField Plugin for Drupal 6 node reference fields.
*
* @internal
*/
#[MigrateField(
id: 'nodereference',
core: [6],
type_map: [
'nodereference' => 'entity_reference',
],
source_module: 'nodereference',
destination_module: 'core',
)]
class NodeReference extends ReferenceBase {
/**
* The plugin ID for the reference type migration.
*
* @var string
*/
protected $nodeTypeMigration = 'd6_node_type';
/**
* {@inheritdoc}
*/
protected function getEntityTypeMigrationId() {
return $this->nodeTypeMigration;
}
/**
* {@inheritdoc}
*/
protected function entityId() {
return 'nid';
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\field\d6;
// cspell:ignore userreference
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\ReferenceBase;
/**
* MigrateField Plugin for Drupal 6 user reference fields.
* @internal
*/
#[MigrateField(
id: 'userreference',
core: [6],
type_map: [
'userreference' => 'entity_reference',
],
source_module: 'userreference',
destination_module: 'core',
)]
class UserReference extends ReferenceBase {
/**
* The plugin ID for the reference type migration.
*
* @var string
*/
protected $userTypeMigration = 'd6_user_role';
/**
* {@inheritdoc}
*/
protected function getEntityTypeMigrationId() {
return $this->userTypeMigration;
}
/**
* {@inheritdoc}
*/
protected function entityId() {
return 'uid';
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => [
'plugin' => 'migration_lookup',
'migration' => 'd6_user',
'source' => 'uid',
],
],
];
$migration->setProcessOfProperty($field_name, $process);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\field\d7;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\ReferenceBase;
/**
* MigrateField plugin for Drupal 7 node_reference fields.
*/
#[MigrateField(
id: 'node_reference',
core: [7],
type_map: [
'node_reference' => 'entity_reference',
],
source_module: 'node_reference',
destination_module: 'core',
)]
class NodeReference extends ReferenceBase {
/**
* The plugin ID for the reference type migration.
*
* @var string
*/
protected $nodeTypeMigration = 'd7_node_type';
/**
* {@inheritdoc}
*/
protected function getEntityTypeMigrationId() {
return $this->nodeTypeMigration;
}
/**
* {@inheritdoc}
*/
protected function entityId() {
return 'nid';
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'node_reference_default' => 'entity_reference_label',
'node_reference_plain' => 'entity_reference_label',
'node_reference_nid' => 'entity_reference_entity_id',
'node_reference_node' => 'entity_reference_entity_view',
'node_reference_path' => 'entity_reference_label',
];
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\field\d7;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\ReferenceBase;
/**
* MigrateField plugin for Drupal 7 user_reference fields.
*/
#[MigrateField(
id: 'user_reference',
core: [7],
type_map: [
'user_reference' => 'entity_reference',
],
source_module: 'user_reference',
destination_module: 'core',
)]
class UserReference extends ReferenceBase {
/**
* The plugin ID for the reference type migration.
*
* @var string
*/
protected $userTypeMigration = 'd7_user_role';
/**
* {@inheritdoc}
*/
protected function getEntityTypeMigrationId() {
return $this->userTypeMigration;
}
/**
* {@inheritdoc}
*/
protected function entityId() {
return 'uid';
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'user_reference_default' => 'entity_reference_label',
'user_reference_plain' => 'entity_reference_label',
'user_reference_uid' => 'entity_reference_entity_id',
'user_reference_user' => 'entity_reference_entity_view',
'user_reference_path' => 'entity_reference_label',
];
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => [
'plugin' => 'migration_lookup',
'migration' => 'd7_user',
'source' => 'uid',
],
],
];
$migration->setProcessOfProperty($field_name, $process);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Returns only the nid from migration_lookup on node_complete migration.
*
* It is possible that migration_lookups that use the classic node migrations
* in the migration key have been altered to include the complete node
* migration. The classic node migration and complete node migration have a
* different number of destination keys. This process plugin will ensure that
* when the complete node migration is used in the lookup the nid value is
* returned. This keeps the behavior the same as the classic node migration.
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess('node_complete_node_lookup')]
class NodeCompleteNodeLookup extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_array($value) && count($value) === 3) {
return $value[0];
}
return $value;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Returns only the vid from migration_lookup on node_complete migration.
*
* It is possible that migration_lookups that use the classic node migrations
* in the migration key have been altered to include the complete node
* migration. The classic node migration and complete node migration have a
* different number of destination keys. This process plugin will ensure that
* when the complete node migration is used in the lookup the vid value is
* returned. This keeps the behavior the same as the classic node migration.
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess('node_complete_node_revision_lookup')]
class NodeCompleteNodeRevisionLookup extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_array($value) && count($value) === 3) {
return $value[1];
}
return $value;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Returns nid and langcode from migration_lookup on node_complete migration.
*
* It is possible that migration_lookups that use the classic node migrations
* in the migration key have been altered to include the complete node
* migration. The classic node migration and complete node migration have a
* different number of destination keys. This process plugin will ensure that
* when the complete node migration is used in the lookup the nid and langcode
* values are returned. This keeps the behavior the same as the classic node
* migration.
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess('node_complete_node_translation_lookup')]
class NodeCompleteNodeTranslationLookup extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_array($value) && count($value) === 3) {
// If the language is 'und' then the node was not translated.
if ($value[2] === 'und') {
return NULL;
}
unset($value[1]);
return array_values($value);
}
return $value;
}
}

View File

@@ -0,0 +1,299 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\EntityFieldDefinitionTrait;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Source plugin to get content entities from the current version of Drupal.
*
* This plugin uses the Entity API to export entity data. If the source entity
* type has custom field storage fields or computed fields, this class will need
* to be extended and the new class will need to load/calculate the values for
* those fields.
*
* Available configuration keys:
* - entity_type: The entity type ID of the entities being exported. This is
* calculated dynamically by the deriver so it is only needed if the deriver
* is not utilized, i.e., a custom source plugin.
* - bundle: (optional) If the entity type is bundleable, only return entities
* of this bundle.
* - include_translations: (optional) Indicates if the entity translations
* should be included, defaults to TRUE.
* - add_revision_id: (optional) Indicates if the revision key is added to the
* source IDs, defaults to TRUE.
*
* Examples:
*
* This will return the default revision for all nodes, from every bundle and
* every translation. The revision key is added to the source IDs.
* @code
* source:
* plugin: content_entity:node
* @endcode
*
* This will return the default revision for all nodes, from every bundle and
* every translation. The revision key is not added to the source IDs.
* @code
* source:
* plugin: content_entity:node
* add_revision_id: false
* @endcode
*
* This will only return nodes of type 'article' in their default language.
* @code
* source:
* plugin: content_entity:node
* bundle: article
* include_translations: false
* @endcode
*
* For additional configuration keys, refer to the parent class:
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "content_entity",
* source_module = "migrate_drupal",
* deriver = "\Drupal\migrate_drupal\Plugin\migrate\source\ContentEntityDeriver",
* )
*/
class ContentEntity extends SourcePluginBase implements ContainerFactoryPluginInterface {
use EntityFieldDefinitionTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The entity type definition.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The plugin's default configuration.
*
* @var array
*/
protected $defaultConfiguration = [
'bundle' => NULL,
'include_translations' => TRUE,
'add_revision_id' => TRUE,
];
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
if (empty($plugin_definition['entity_type'])) {
throw new InvalidPluginDefinitionException($plugin_id, 'Missing required "entity_type" definition.');
}
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->entityType = $this->entityTypeManager->getDefinition($plugin_definition['entity_type']);
if (!$this->entityType instanceof ContentEntityTypeInterface) {
throw new InvalidPluginDefinitionException($plugin_id, sprintf('The entity type (%s) is not supported. The "content_entity" source plugin only supports content entities.', $plugin_definition['entity_type']));
}
if (!empty($configuration['bundle'])) {
if (!$this->entityType->hasKey('bundle')) {
throw new \InvalidArgumentException(sprintf('A bundle was provided but the entity type (%s) is not bundleable.', $plugin_definition['entity_type']));
}
$bundle_info = array_keys($this->entityTypeBundleInfo->getBundleInfo($this->entityType->id()));
if (!in_array($configuration['bundle'], $bundle_info, TRUE)) {
throw new \InvalidArgumentException(sprintf('The provided bundle (%s) is not valid for the (%s) entity type.', $configuration['bundle'], $plugin_definition['entity_type']));
}
}
parent::__construct($configuration + $this->defaultConfiguration, $plugin_id, $plugin_definition, $migration);
}
/**
* {@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_type.manager'),
$container->get('entity_field.manager'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function __toString() {
return (string) $this->entityType->getPluralLabel();
}
/**
* Initializes the iterator with the source data.
*
* @return \Generator
* A data generator for this source.
*/
protected function initializeIterator() {
$ids = $this->query()->execute();
return $this->yieldEntities($ids);
}
/**
* Loads and yields entities, one at a time.
*
* @param array $ids
* The entity IDs.
*
* @return \Generator
* An iterable of the loaded entities.
*/
protected function yieldEntities(array $ids) {
$storage = $this->entityTypeManager
->getStorage($this->entityType->id());
foreach ($ids as $id) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $storage->load($id);
yield $this->toArray($entity);
if ($this->configuration['include_translations']) {
foreach ($entity->getTranslationLanguages(FALSE) as $language) {
yield $this->toArray($entity->getTranslation($language->getId()));
}
}
}
}
/**
* Converts an entity to an array.
*
* Makes all IDs into flat values. All other values are returned as per
* $entity->toArray(), which is a nested array.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity to convert.
*
* @return array
* The entity, represented as an array.
*/
protected function toArray(ContentEntityInterface $entity) {
$return = $entity->toArray();
// This is necessary because the IDs must be flat. They cannot be nested for
// the ID map.
foreach (array_keys($this->getIds()) as $id) {
/** @var \Drupal\Core\TypedData\Plugin\DataType\ItemList $value */
$value = $entity->get($id);
// Force the IDs on top of the previous values.
$return[$id] = $value->first()->getString();
}
return $return;
}
/**
* Query to retrieve the entities.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* The query.
*/
public function query() {
$query = $this->entityTypeManager
->getStorage($this->entityType->id())
->getQuery()
->accessCheck(FALSE);
if (!empty($this->configuration['bundle'])) {
$query->condition($this->entityType->getKey('bundle'), $this->configuration['bundle']);
}
// Exclude anonymous user account.
if ($this->entityType->id() === 'user' && !empty($this->entityType->getKey('id'))) {
$query->condition($this->entityType->getKey('id'), 0, '>');
}
return $query;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function count($refresh = FALSE) {
// If no translations are included, then a simple query is possible.
if (!$this->configuration['include_translations']) {
return parent::count($refresh);
}
// @todo Determine a better way to retrieve a valid count for translations.
// https://www.drupal.org/project/drupal/issues/2937166
return MigrateSourceInterface::NOT_COUNTABLE;
}
/**
* {@inheritdoc}
*/
protected function doCount() {
return $this->query()->count()->execute();
}
/**
* {@inheritdoc}
*/
public function fields() {
// Retrieving fields from a non-fieldable content entity will throw a
// LogicException. Return an empty list of fields instead.
if (!$this->entityType->entityClassImplements('Drupal\Core\Entity\FieldableEntityInterface')) {
return [];
}
$field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id());
if (!empty($this->configuration['bundle'])) {
$field_definitions += $this->entityFieldManager->getFieldDefinitions($this->entityType->id(), $this->configuration['bundle']);
}
$fields = array_map(function ($definition) {
return (string) $definition->getLabel();
}, $field_definitions);
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$id_key = $this->entityType->getKey('id');
$ids[$id_key] = $this->getDefinitionFromEntity($id_key);
if ($this->configuration['add_revision_id'] && $this->entityType->isRevisionable()) {
$revision_key = $this->entityType->getKey('revision');
$ids[$revision_key] = $this->getDefinitionFromEntity($revision_key);
}
if ($this->entityType->isTranslatable()) {
$langcode_key = $this->entityType->getKey('langcode');
$ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key);
}
return $ids;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for content entity source plugins.
*/
class ContentEntityDeriver extends DeriverBase implements ContainerDeriverInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new ContentEntityDeriver.
*
* @param string $base_plugin_id
* The base plugin ID.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct($base_plugin_id, EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $id => $definition) {
if ($definition instanceof ContentEntityTypeInterface) {
$this->derivatives[$id] = $base_plugin_definition;
// Provide entity_type so the source can be used apart from a deriver.
$this->derivatives[$id]['entity_type'] = $id;
}
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A base class for source plugins using a Drupal database as a source.
*
* Provides general purpose helper methods that are commonly needed
* when writing source plugins that use a Drupal database as a source, for
* example:
* - Check if the given module exists in the source database.
* - Read Drupal configuration variables from the source database.
*
* For a full list, refer to the methods of this class.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*/
abstract class DrupalSqlBase extends SqlBase implements DependentPluginInterface {
use DependencyTrait;
/**
* The contents of the system table.
*
* @var array
*/
protected $systemData;
/**
* If the source provider is missing.
*
* @var bool
*/
protected $requirements = TRUE;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state);
$this->entityTypeManager = $entity_type_manager;
}
/**
* Retrieves all system data information from the source Drupal database.
*
* @return array
* List of system table information keyed by type and name.
*/
public function getSystemData() {
if (!isset($this->systemData)) {
$this->systemData = [];
try {
$results = $this->select('system', 's')
->fields('s')
->execute();
foreach ($results as $result) {
$this->systemData[$result['type']][$result['name']] = $result;
}
}
catch (\Exception $e) {
// The table might not exist for example in tests.
}
}
return $this->systemData;
}
/**
* {@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('state'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
parent::checkRequirements();
if ($this->pluginDefinition['requirements_met'] === TRUE) {
if ($source_module = $this->getSourceModule()) {
if ($this->moduleExists($source_module)) {
if (isset($this->pluginDefinition['minimum_version'])) {
$minimum_version = (int) $this->pluginDefinition['minimum_version'];
$installed_version = (int) $this->getModuleSchemaVersion($source_module);
if ($minimum_version > $installed_version) {
throw new RequirementsException('Required minimum version ' . $this->pluginDefinition['minimum_version'], ['minimum_version' => $this->pluginDefinition['minimum_version']]);
}
}
}
else {
throw new RequirementsException('The module ' . $source_module . ' is not enabled in the source site.', ['source_module' => $source_module]);
}
}
}
}
/**
* Retrieves a module schema_version from the source Drupal database.
*
* @param string $module
* Name of module.
*
* @return mixed
* The current module schema version on the origin system table or FALSE if
* not found.
*/
protected function getModuleSchemaVersion($module) {
$system_data = $this->getSystemData();
return $system_data['module'][$module]['schema_version'] ?? FALSE;
}
/**
* Checks if a given module is enabled in the source Drupal database.
*
* @param string $module
* Name of module to check.
*
* @return bool
* TRUE if module is enabled on the origin system, FALSE if not.
*/
protected function moduleExists($module) {
$system_data = $this->getSystemData();
return !empty($system_data['module'][$module]['status']);
}
/**
* Reads a variable from a source Drupal database.
*
* @param $name
* Name of the variable.
* @param $default
* The default value.
*
* @return mixed
*/
protected function variableGet($name, $default) {
try {
$result = $this->select('variable', 'v')
->fields('v', ['value'])
->condition('name', $name)
->execute()
->fetchField();
}
// The table might not exist.
catch (\Exception $e) {
$result = FALSE;
}
return $result !== FALSE ? unserialize($result) : $default;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
// Generic handling for Drupal source plugin constants.
if (isset($this->configuration['constants']['entity_type'])) {
$this->addDependency('module', $this->entityTypeManager->getDefinition($this->configuration['constants']['entity_type'])->getProvider());
}
if (isset($this->configuration['constants']['module'])) {
$this->addDependency('module', $this->configuration['constants']['module']);
}
return $this->dependencies;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Entity\DependencyTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\source\EmptySource as BaseEmptySource;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* Source returning an empty row with Drupal specific config dependencies.
*
* For more information and available configuration keys, refer to the parent
* classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\EmptySource
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "md_empty",
* source_module = "system",
* )
*/
class EmptySource extends BaseEmptySource implements ContainerFactoryPluginInterface, DependentPluginInterface {
use DependencyTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->entityTypeManager = $entity_type_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('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
// The empty source plugin supports the entity_type constant.
if (isset($this->configuration['constants']['entity_type'])) {
$this->addDependency('module', $this->entityTypeManager->getDefinition($this->configuration['constants']['entity_type'])->getProvider());
}
return $this->dependencies;
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Drupal 6/7 variable source from database.
*
* This source class fetches variables from the source Drupal database.
* Depending on the configuration, this returns zero or a single row and as such
* is not a good example for any normal source class returning multiple rows.
*
* Available configuration keys (one of which must be defined):
* - variables: (optional) The list of variables to retrieve from the source
* database. Specified variables are retrieved in a single row.
* - variables_no_row_if_missing: (optional) The list of variables to retrieve
* from the source database. If any of the variables listed here are missing
* in the source, then the source will return zero rows.
*
* Examples:
*
* With this configuration, the source will return one row even when the
* "filter_fallback_format" variable isn't available:
* @code
* source:
* plugin: variable
* variables:
* - filter_fallback_format
* @endcode
*
* With this configuration, the source will return one row if the variable is
* available, and zero if it isn't:
* @code
* source:
* plugin: variable
* variables_no_row_if_missing:
* - filter_fallback_format
* @endcode
*
* The variables and the variables_no_row_if_missing lists are always merged
* together. All of the following configurations are valid:
* @code
* source:
* plugin: variable
* variables:
* - book_child_type
* - book_block_mode
* - book_allowed_types
* variables_no_row_if_missing:
* - book_child_type
* - book_block_mode
* - book_allowed_types
*
* source:
* plugin: variable
* variables:
* - book_child_type
* - book_block_mode
* variables_no_row_if_missing:
* - book_allowed_types
*
* source:
* plugin: variable
* variables_no_row_if_missing:
* - book_child_type
* - book_block_mode
* - book_allowed_types
* @endcode
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "variable",
* source_module = "system",
* )
*/
class Variable extends DrupalSqlBase {
/**
* The variable names to fetch.
*
* @var array
*/
protected $variables;
/**
* The variables that result in no row if any are missing from the source.
*
* @var array
*/
protected $variablesNoRowIfMissing;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_type_manager);
$this->variablesNoRowIfMissing = $this->configuration['variables_no_row_if_missing'] ?? [];
$variables = $this->configuration['variables'] ?? [];
$this->variables = array_unique(array_merge(array_values($variables), array_values($this->variablesNoRowIfMissing)));
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
if ($this->count()) {
return new \ArrayIterator([$this->values()]);
}
return new \ArrayIterator();
}
/**
* Return the values of the variables specified in the plugin configuration.
*
* @return array
* An associative array where the keys are the variables specified in the
* plugin configuration and the values are the values found in the source.
* Only those values are returned that are actually in the database.
*/
protected function values() {
// Create an ID field so we can record migration in the map table.
// Arbitrarily, use the first variable name.
$values['id'] = reset($this->variables);
return $values + array_map('unserialize', $this->prepareQuery()->execute()->fetchAllKeyed());
}
/**
* {@inheritdoc}
*/
protected function doCount() {
if (empty($this->variablesNoRowIfMissing)) {
return 1;
}
$variable_names = array_keys($this->query()->execute()->fetchAllAssoc('name'));
if (!empty(array_diff($this->variablesNoRowIfMissing, $variable_names))) {
return 0;
}
return 1;
}
/**
* {@inheritdoc}
*/
public function fields() {
return array_combine($this->variables, $this->variables);
}
/**
* {@inheritdoc}
*/
public function query() {
return $this->getDatabase()
->select('variable', 'v')
->fields('v', ['name', 'value'])
->condition('name', $this->variables, 'IN');
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['id']['type'] = 'string';
return $ids;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source;
use Drupal\migrate\Row;
// cspell:ignore multirow
/**
* Drupal 6/7 multiple variables source from database.
*
* Unlike the variable source plugin, this one returns one row per
* variable.
*
* Available configuration keys:
* - variables: (required) The list of variables to retrieve from the source
* database. Each variable is retrieved in a separate row.
*
* Example:
*
* @code
* plugin: variable_multirow
* variables:
* - date_format_long
* - date_format_medium
* - date_format_short
* @endcode
*
* In this example the specified variables are retrieved from the source
* database one row per variable.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "variable_multirow",
* source_module = "system",
* )
*/
class VariableMultiRow extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('variable', 'v')
->fields('v', ['name', 'value'])
// Cast scalars to array so we can consistently use an IN condition.
->condition('name', (array) $this->configuration['variables'], 'IN');
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'name' => $this->t('Name'),
'value' => $this->t('Value'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
if ($value = $row->getSourceProperty('value')) {
$row->setSourceProperty('value', unserialize($value));
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['name']['type'] = 'string';
return $ids;
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source\d6;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 i18n_variable source from database.
*
* Available configuration keys:
* - variables: (required) The list of variable translations to retrieve from
* the source database. All translations are retrieved in a single row.
*
* Examples:
*
* @code
* plugin: d6_variable_translation
* variables:
* - site_offline_message
* @endcode
* In this example the translations for site_offline_message variable are
* retrieved from the source database.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_variable_translation",
* source_module = "i18n",
* )
*/
class VariableTranslation extends DrupalSqlBase {
/**
* The variable names to fetch.
*
* @var array
*/
protected $variables;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_type_manager);
$this->variables = $this->configuration['variables'];
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
return new \ArrayIterator($this->values());
}
/**
* Return the values of the variables specified in the plugin configuration.
*
* @return array
* An associative array where the keys are the variables specified in the
* plugin configuration and the values are the values found in the source.
* A key/value pair is added for the language code. Only those values are
* returned that are actually in the database.
*/
protected function values() {
$values = [];
$result = $this->prepareQuery()->execute()->FetchAllAssoc('language');
foreach ($result as $i18n_variable) {
$values[]['language'] = $i18n_variable->language;
}
$result = $this->prepareQuery()->execute()->FetchAll();
foreach ($result as $i18n_variable) {
foreach ($values as $key => $value) {
if ($values[$key]['language'] === $i18n_variable->language) {
$values[$key][$i18n_variable->name] = unserialize($i18n_variable->value);
break;
}
}
}
return $values;
}
/**
* {@inheritdoc}
*/
protected function doCount() {
return $this->initializeIterator()->count();
}
/**
* {@inheritdoc}
*/
public function fields() {
return array_combine($this->variables, $this->variables);
}
/**
* {@inheritdoc}
*/
public function query() {
return $this->getDatabase()
->select('i18n_variable', 'v')
->fields('v')
->condition('name', (array) $this->configuration['variables'], 'IN');
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
if (!$this->getDatabase()->schema()->tableExists('i18n_variable')) {
throw new RequirementsException("Source database table 'i18n_variable' does not exist");
}
parent::checkRequirements();
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source\d7;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Base class for D7 source plugins which need to collect field values.
*
* Field values are collected from the Field API.
*
* Refer to the existing implementations for examples:
* @see \Drupal\node\Plugin\migrate\source\d7\Node
* @see \Drupal\user\Plugin\migrate\source\d7\User
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*/
abstract class FieldableEntity extends DrupalSqlBase {
/**
* Cached field and field instance definitions.
*
* @var array
*/
protected $fieldInfo;
/**
* Returns all non-deleted field instances attached to a specific entity type.
*
* Typically, getFields() is used in the prepareRow method of a source plugin
* to get a list of all the field instances of the entity. A source plugin can
* then loop through the list of fields to do any other preparation before
* processing the row. Typically, a source plugin will use getFieldValues()
* to get the values of each field.
*
* @param string $entity_type
* The entity type ID.
* @param string|null $bundle
* (optional) The bundle.
*
* @return array[]
* The field instances, keyed by field name.
*/
protected function getFields($entity_type, $bundle = NULL) {
$cid = $entity_type . ':' . ($bundle ?? '');
if (!isset($this->fieldInfo[$cid])) {
$query = $this->select('field_config_instance', 'fci')
->fields('fci')
->condition('fci.entity_type', $entity_type)
->condition('fci.bundle', $bundle ?? $entity_type)
->condition('fci.deleted', 0);
// Join the 'field_config' table and add the 'translatable' setting to the
// query.
$query->leftJoin('field_config', 'fc', '[fci].[field_id] = [fc].[id]');
$query->addField('fc', 'translatable');
$this->fieldInfo[$cid] = $query->execute()->fetchAllAssoc('field_name');
}
return $this->fieldInfo[$cid];
}
/**
* Retrieves field values for a single field of a single entity.
*
* Typically, getFieldValues() is used in the prepareRow method of a source
* plugin where the return values are placed on the row source.
*
* @param string $entity_type
* The entity type.
* @param string $field
* The field name.
* @param int $entity_id
* The entity ID.
* @param int|null $revision_id
* (optional) The entity revision ID.
* @param string $language
* (optional) The field language.
*
* @return array
* The raw field values, keyed and sorted by delta.
*/
protected function getFieldValues($entity_type, $field, $entity_id, $revision_id = NULL, $language = NULL) {
$table = (isset($revision_id) ? 'field_revision_' : 'field_data_') . $field;
$query = $this->select($table, 't')
->fields('t')
->condition('entity_type', $entity_type)
->condition('entity_id', $entity_id)
->condition('deleted', 0)
->orderBy('delta');
if (isset($revision_id)) {
$query->condition('revision_id', $revision_id);
}
// Add 'language' as a query condition if it has been defined by Entity
// Translation.
if ($language) {
$query->condition('language', $language);
}
$values = [];
foreach ($query->execute() as $row) {
foreach ($row as $key => $value) {
$delta = $row['delta'];
if (str_starts_with($key, $field)) {
$column = substr($key, strlen($field) + 1);
$values[$delta][$column] = $value;
}
}
}
return $values;
}
/**
* Checks if an entity type uses Entity Translation.
*
* @param string $entity_type
* The entity type.
*
* @return bool
* Whether the entity type uses entity translation.
*/
protected function isEntityTranslatable($entity_type) {
return in_array($entity_type, $this->variableGet('entity_translation_entity_types', []), TRUE);
}
/**
* Gets an entity source language from the 'entity_translation' table.
*
* @param string $entity_type
* The entity type.
* @param int $entity_id
* The entity ID.
*
* @return string|bool
* The entity source language or FALSE if no source language was found.
*/
protected function getEntityTranslationSourceLanguage($entity_type, $entity_id) {
try {
return $this->select('entity_translation', 'et')
->fields('et', ['language'])
->condition('entity_type', $entity_type)
->condition('entity_id', $entity_id)
->condition('source', '')
->execute()
->fetchField();
}
// The table might not exist.
catch (\Exception $e) {
return FALSE;
}
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source\d7;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 variable_store source from database.
*
* Available configuration keys:
* - variables: (required) The list of variable translations to retrieve from
* the source database. All translations are retrieved in a single row.
*
* Example:
*
* @code
* plugin: d7_variable_translation
* variables:
* - site_name
* - site_slogan
* @endcode
* In this example the translations for site_name and site_slogan variables are
* retrieved from the source database.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_variable_translation",
* source_module = "i18n_variable",
* )
*/
class VariableTranslation extends DrupalSqlBase {
/**
* The variable names to fetch.
*
* @var array
*/
protected $variables;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_type_manager);
$this->variables = $this->configuration['variables'];
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
return new \ArrayIterator($this->values());
}
/**
* Return the values of the variables specified in the plugin configuration.
*
* @return array
* An associative array where the keys are the variables specified in the
* plugin configuration and the values are the values found in the source.
* A key/value pair is added for the language code. Only those values are
* returned that are actually in the database.
*/
protected function values() {
$values = [];
$result = $this->prepareQuery()->execute()->FetchAllAssoc('realm_key');
foreach ($result as $variable_store) {
$values[]['language'] = $variable_store['realm_key'];
}
$result = $this->prepareQuery()->execute()->FetchAll();
foreach ($result as $variable_store) {
foreach ($values as $key => $value) {
if ($values[$key]['language'] === $variable_store['realm_key']) {
if ($variable_store['serialized']) {
$values[$key][$variable_store['name']] = unserialize($variable_store['value']);
break;
}
else {
$values[$key][$variable_store['name']] = $variable_store['value'];
break;
}
}
}
}
return $values;
}
/**
* {@inheritdoc}
*/
protected function doCount() {
return $this->initializeIterator()->count();
}
/**
* {@inheritdoc}
*/
public function fields() {
return array_combine($this->variables, $this->variables);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('variable_store', 'vs')
->fields('vs')
->condition('realm', 'language')
->condition('name', (array) $this->configuration['variables'], 'IN');
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Drupal\migrate_drupal\Plugin\migrate\source\d8;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 8+ configuration source from database.
*
* Available configuration keys:
* - collections: (optional) The collection of configuration storage to retrieve
* from the source - can be a string or an array. If omitted, configuration
* objects of all available collections are retrieved.
* - names: (optional) Names of configuration objects to retrieve from the
* source - can be a string or an array. If omitted, all available
* configuration objects are retrieved.
*
* Examples:
*
* @code
* source:
* plugin: d8_config
* names:
* - node.type.article
* - node.type.page
* @endcode
*
* In this example configuration objects of article and page content types are
* retrieved from the source database.
*
* @code
* source:
* plugin: d8_config
* collections: language.fr
* names:
* - node.type.article
* - node.type.page
* @endcode
*
* In this example configuration objects are filtered by language.fr collection.
* As a result, French versions of specified configuration objects are retrieved
* from the source database.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d8_config",
* source_module = "system",
* )
*/
class Config extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('config', 'c')
->fields('c', ['collection', 'name', 'data']);
if (!empty($this->configuration['collections'])) {
$query->condition('collection', (array) $this->configuration['collections'], 'IN');
}
if (!empty($this->configuration['names'])) {
$query->condition('name', (array) $this->configuration['names'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('data', unserialize($row->getSourceProperty('data')));
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'collection' => $this->t('The config object collection.'),
'name' => $this->t('The config object name.'),
'data' => $this->t('Serialized configuration object data.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['collection']['type'] = 'string';
$ids['name']['type'] = 'string';
return $ids;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Drupal\migrate_drupal\Tests;
use Drupal\migrate\Row;
/**
* Provides common functionality for testing stubbing.
*/
trait StubTestTrait {
/**
* Tests that creating a stub of an entity type results in a valid entity.
*
* @param string $entity_type_id
* The entity type we are stubbing.
*/
protected function performStubTest($entity_type_id) {
$entity_id = $this->createEntityStub($entity_type_id);
$this->assertNotEmpty($entity_id, 'Stub successfully created');
// When validateStub fails, it will return an array with the violations.
$this->assertEmpty($this->validateStub($entity_type_id, $entity_id));
}
/**
* Create a stub of the given entity type.
*
* @param string $entity_type_id
* The entity type we are stubbing.
*
* @return int
* ID of the created entity.
*/
protected function createEntityStub($entity_type_id) {
// Create a dummy migration to pass to the destination plugin.
$definition = [
'migration_tags' => ['Stub test'],
'source' => ['plugin' => 'empty'],
'process' => [],
'destination' => ['plugin' => 'entity:' . $entity_type_id],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$destination_plugin = $migration->getDestinationPlugin(TRUE);
$stub_row = new Row([], [], TRUE);
$destination_ids = $destination_plugin->import($stub_row);
return reset($destination_ids);
}
/**
* Perform validation on a stub entity.
*
* @param string $entity_type_id
* The entity type we are stubbing.
* @param string $entity_id
* ID of the stubbed entity to validate.
*
* @return \Drupal\Core\Entity\EntityConstraintViolationListInterface
* List of constraint violations identified.
*/
protected function validateStub($entity_type_id, $entity_id) {
$controller = \Drupal::entityTypeManager()->getStorage($entity_type_id);
/** @var \Drupal\Core\Entity\ContentEntityInterface $stub_entity */
$stub_entity = $controller->load($entity_id);
return $stub_entity->validate();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
name: 'Migrate drupal field discovery tet'
type: module
description: 'Module containing a test class exposing protected field discovery methods'
package: Testing
# 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,93 @@
<?php
namespace Drupal\field_discovery_test;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_drupal\FieldDiscovery;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
use Psr\Log\LoggerInterface;
/**
* A test class to expose protected methods.
*/
class FieldDiscoveryTestClass extends FieldDiscovery {
/**
* An array of test data.
*
* @var array
*/
protected $testData;
/**
* Constructs a FieldDiscoveryTestClass object.
*
* @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_plugin_manager
* The field plugin manager.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
* @param array $test_data
* An array of test data, keyed by method name, for overridden methods to
* return for the purposes of testing other methods.
*/
public function __construct(MigrateFieldPluginManagerInterface $field_plugin_manager, MigrationPluginManagerInterface $migration_plugin_manager, LoggerInterface $logger, array $test_data = []) {
parent::__construct($field_plugin_manager, $migration_plugin_manager, $logger);
$this->testData = $test_data;
}
/**
* {@inheritdoc}
*/
public function getAllFields($core) {
if (!empty($this->testData['getAllFields'][$core])) {
return $this->testData['getAllFields'][$core];
}
return parent::getAllFields($core);
}
/**
* {@inheritdoc}
*/
public function getBundleFields($core, $entity_type_id, $bundle) {
return parent::getBundleFields($core, $entity_type_id, $bundle);
}
/**
* {@inheritdoc}
*/
public function getEntityFields($core, $entity_type_id) {
return parent::getEntityFields($core, $entity_type_id);
}
/**
* {@inheritdoc}
*/
public function getFieldInstanceStubMigrationDefinition($core) {
return parent::getFieldInstanceStubMigrationDefinition($core);
}
/**
* {@inheritdoc}
*/
public function getCoreVersion(MigrationInterface $migration) {
return parent::getCoreVersion($migration);
}
/**
* {@inheritdoc}
*/
public function getFieldPlugin($field_type, MigrationInterface $migration) {
return parent::getFieldPlugin($field_type, $migration);
}
/**
* {@inheritdoc}
*/
public function getSourcePlugin($core) {
return parent::getSourcePlugin($core);
}
}

View File

@@ -0,0 +1,10 @@
name: 'Migrate field plugin manager test'
type: module
description: 'Example module demonstrating the field plugin manager in the Migrate API.'
package: Testing
# 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,20 @@
<?php
namespace Drupal\migrate_field_plugin_manager_test\Plugin\migrate\field;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
/**
* MigrateField Plugin for Drupal 6 file fields.
*/
#[MigrateField(
id: 'd6_file',
core: [6],
type_map: [
'file' => 'file',
],
source_module: 'foo',
destination_module: 'bar',
)]
class D6FileField extends FieldPluginBase {}

View File

@@ -0,0 +1,13 @@
<?php
namespace Drupal\migrate_field_plugin_manager_test\Plugin\migrate\field;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
#[MigrateField(
id: 'd6_no_core_version_specified',
source_module: 'foo',
destination_module: 'bar',
)]
class D6NoCoreVersionSpecified extends FieldPluginBase {}

View File

@@ -0,0 +1,10 @@
name: 'Migrate property overwrite test'
type: module
description: 'Example module demonstrating property overwrite support in the Migrate API.'
package: Testing
# 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,31 @@
id: users
label: User migration
migration_tags:
- Drupal 6
- Drupal 7
source:
plugin: d6_user
process:
# If the entity's ID is migrated, the Migrate API will try to update
# an existing entity with that ID. If no entity with that ID already
# exists, it will be created.
uid: uid
name: name
mail: mail
password: password
'signature/value':
plugin: default_value
default_value: 'The answer is 42.'
destination:
plugin: entity:user
# If the destination is going to update an existing user, you can optionally
# specify the properties that should be overwritten. For example, if the
# migration tries to import user 31 and user 31 already exists in the
# destination database, only the 'name' and 'mail' properties of the user
# will be overwritten. If user 31 doesn't exist, it will be created and
# the overwrite_properties list will be ignored.
overwrite_properties:
- name
- mail
# It's possible to overwrite nested properties too.
- 'signature/value'

View File

@@ -0,0 +1,10 @@
name: Migrate state active test
type: module
description: Tests the 'active' migrate state
package: Testing
# 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,19 @@
id: migrate_state_finished_test
label: Block content body field configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: embedded_data
data_rows:
-
id: 1
ids:
id:
type: string
source_module: action
process: []
destination:
plugin: entity:field_config
destination_module: migrate_state_finished_test

View File

@@ -0,0 +1,19 @@
id: migrate_state_finished_test1
label: Block content body field configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: embedded_data
data_rows:
-
id: 1
ids:
id:
type: string
source_module: action
process: []
destination:
plugin: entity:field_config
destination_module: migrate_state_not_finished_test

View File

@@ -0,0 +1,14 @@
finished:
6:
action:
- migrate_state_finished_test
- migrate_state_not_finished_test
7:
# Migrations
action:
- migrate_state_finished_test
- migrate_state_not_finished_test
not_finished:
7:
# Migrations
action: system

View File

@@ -0,0 +1,10 @@
name: Migrate state no migration and no migrate_drupal.yml file test
type: module
description: Does not have a migration or migrate_drupal.yml file.
package: Testing
# 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,10 @@
name: Migrate state incomplete test
type: module
description: Tests the 'incomplete' migrate state
package: Testing
# 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,20 @@
id: migrate_state_not_finished_test
label: Migrate state incomplete test
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: embedded_data
data_rows:
-
entity_type: block_content
ids:
entity_type:
type: string
source_module: block
process:
entity_type: entity_type
destination:
plugin: entity:field_config
destination_module: migrate_state_not_finished_test

View File

@@ -0,0 +1,12 @@
# cspell:ignore optionwidgets
not_finished:
6:
block: migrate_state_not_finished_test
# Override any finished declarations for this field plugin.
optionwidgets: options
7:
# Override any finished declarations for this field plugin.
options: options
# Override any finished declarations for this migration.
action: migrate_state_not_finished_test
block: migrate_state_not_finished_test

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for migrate_drupal.
*
* @group migrate_drupal
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Test that no dummy migrate_map tables are created.
*
* @group migrate_drupal
*/
class IdMapTableNoDummyTest extends MigrateDrupal6TestBase {
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->pluginManager = $this->container->get('plugin.manager.migration');
$this->pluginManager->createInstance('d6_user');
}
/**
* Tests that dummy map tables do not exist.
*/
public function testNoDummyTables(): void {
$database = \Drupal::database();
$tables = $database->schema()->findTables('%migrate_map%');
$dummy_tables = preg_grep("/.*migrate_map_([0-9a-fA-F]){13}/", $tables);
$this->assertCount(0, $dummy_tables);
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel;
use Drupal\Core\Database\Database;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
/**
* Base class for Drupal migration tests.
*/
abstract class MigrateDrupalTestBase extends MigrateTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'system',
'user',
'field',
'migrate_drupal',
'options',
'file',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$module_handler = \Drupal::moduleHandler();
if ($module_handler->moduleExists('node')) {
$this->installEntitySchema('node');
}
if ($module_handler->moduleExists('comment')) {
$this->installEntitySchema('comment');
}
if ($module_handler->moduleExists('taxonomy')) {
$this->installEntitySchema('taxonomy_term');
}
if ($module_handler->moduleExists('user')) {
$this->installEntitySchema('user');
}
$this->installConfig(['migrate_drupal', 'system']);
}
/**
* Loads a database fixture into the source database connection.
*
* @param string $path
* Path to the dump file.
*/
protected function loadFixture($path) {
$default_db = Database::getConnection()->getKey();
Database::setActiveConnection($this->sourceDatabase->getKey());
if (str_ends_with($path, '.gz')) {
$path = 'compress.zlib://' . $path;
}
require $path;
Database::setActiveConnection($default_db);
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
/**
* Tests the field plugin manager.
*
* @group migrate_drupal
* @coversDefaultClass \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManager
*/
class MigrateFieldPluginManagerTest extends MigrateDrupalTestBase {
/**
* The field plugin manager.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
protected static $modules = [
'datetime',
'system',
'user',
'field',
'migrate_drupal',
'options',
'file',
'image',
'text',
'link',
'migrate_field_plugin_manager_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->pluginManager = $this->container->get('plugin.manager.migrate.field');
}
/**
* Tests that the correct MigrateField plugins are used.
*
* @covers ::getPluginIdFromFieldType
*/
public function testPluginSelection(): void {
$this->assertSame('link', $this->pluginManager->getPluginIdFromFieldType('link', ['core' => 6]));
$this->assertSame('link_field', $this->pluginManager->getPluginIdFromFieldType('link_field', ['core' => 7]));
$this->assertSame('image', $this->pluginManager->getPluginIdFromFieldType('image', ['core' => 7]));
$this->assertSame('file', $this->pluginManager->getPluginIdFromFieldType('file', ['core' => 7]));
$this->assertSame('d6_file', $this->pluginManager->getPluginIdFromFieldType('file', ['core' => 6]));
$this->assertSame('d6_text', $this->pluginManager->getPluginIdFromFieldType('text', ['core' => 6]));
$this->assertSame('d7_text', $this->pluginManager->getPluginIdFromFieldType('text', ['core' => 7]));
// Test that the deprecated d6 'date' plugin is not returned.
$this->assertSame('datetime', $this->pluginManager->getPluginIdFromFieldType('date', ['core' => 6]));
// Test fallback when no core version is specified.
$this->assertSame('d6_no_core_version_specified', $this->pluginManager->getPluginIdFromFieldType('d6_no_core_version_specified', ['core' => 6]));
}
/**
* Tests that a PluginNotFoundException is thrown when a plugin isn't found.
*
* @covers ::getPluginIdFromFieldType
* @dataProvider nonExistentPluginExceptionsData
*/
public function testNonExistentPluginExceptions($core, $field_type): void {
$this->expectException(PluginNotFoundException::class);
$this->expectExceptionMessage(sprintf("Plugin ID '%s' was not found.", $field_type));
$this->pluginManager->getPluginIdFromFieldType($field_type, ['core' => $core]);
}
/**
* Provides data for testNonExistentPluginExceptions.
*
* @return array
* The data.
*/
public static function nonExistentPluginExceptionsData() {
return [
'D7 Filefield' => [
'core' => 7,
'field_type' => 'filefield',
],
'D6 linkfield' => [
'core' => 6,
'field_type' => 'link_field',
],
'D7 link' => [
'core' => 7,
'field_type' => 'link',
],
'D7 no core version' => [
'core' => 7,
'field_type' => 'd6_no_core_version_specified',
],
];
}
/**
* Tests that plugins with no explicit weight are given a weight of 0.
*/
public function testDefaultWeight(): void {
$definitions = $this->pluginManager->getDefinitions();
$deprecated_plugins = [
'date',
];
foreach ($definitions as $id => $definition) {
$this->assertArrayHasKey('weight', $definition);
if (in_array($id, $deprecated_plugins, TRUE)) {
$this->assertSame(9999999, $definition['weight']);
}
else {
$this->assertSame(0, $definition['weight']);
}
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests that a migration can be instantiated without a database connection.
*
* @group migrate_drupal
*/
class MigrateMissingDatabaseTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_drupal', 'node'];
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
// Set the 'migrate' database connection to use a missing host.
$info = Database::getConnectionInfo('default')['default'];
$info['host'] = 'does_not_exist';
Database::addConnectionInfo('migrate', 'default', $info);
}
/**
* Tests that a migration can be instantiated with the node module enabled.
*
* When the migrate_drupal and node modules are enabled, the migration
* derivers call checkRequirements() whenever createInstance() is used. If the
* database connection is not available, then Migration::setUpDatabase()
* throws an exception. Check that the exception is caught and the migration
* can still be used to access its IdMap.
*/
public function testMissingDatabase(): void {
$migration = $this->migrationPluginManager->createInstance('d7_node_type');
$this->assertInstanceOf(MigrationInterface::class, $migration);
$this->assertInstanceOf(MigrateIdMapInterface::class, $migration->getIdMap());
}
}

View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel;
use Drupal\migrate_drupal\NodeMigrateType;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
use Drupal\Tests\migrate_drupal\Traits\NodeMigrateTypeTestTrait;
/**
* Tests the assignment of the node migration type in migrations_plugin_alter.
*
* @group migrate_drupal
*/
class NodeMigrationTypePluginAlterTest extends MigrateTestBase {
use NodeMigrateTypeTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal', 'node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setupDb();
}
/**
* Tests the assignment of the node migration type.
*
* @param string $type
* The type of node migration, 'classic' or 'complete'.
* @param array $migration_definitions
* An array of migration definitions.
* @param array $expected
* The expected results.
*
* @dataProvider providerMigrationPluginAlter
*
* @throws \Exception
*/
public function testMigrationPluginAlter($type, array $migration_definitions, array $expected): void {
$this->makeNodeMigrateMapTable($type, '7');
migrate_drupal_migration_plugins_alter($migration_definitions);
$this->assertSame($expected, $migration_definitions);
}
/**
* Data provider for testMigrationPluginAlter().
*/
public static function providerMigrationPluginAlter() {
$tests = [];
$migrations = [
// The 'system_site' migration is needed to get the legacy Drupal version.
'system_site' => [
'id' => 'system_site',
'source' => [
'plugin' => 'variable',
'variables' => [
'site_name',
'site_mail',
],
'source_module' => 'system',
],
'process' => [],
],
'no_dependencies_not_altered' => [
'id' => 'no_dependencies_not_altered',
'no_dependencies' => 'test',
'process' => [
'nid' => 'nid',
],
],
'dependencies_altered_if_complete' => [
'id' => 'test',
'migration_dependencies' => [
'required' => [
'd7_node',
],
'optional' => [
'd7_node_translation',
],
],
],
'dependencies_not_altered' => [
'id' => 'd7_node',
'migration_dependencies' => [
'required' => [
'd7_node',
],
'optional' => [
'd7_node_translation',
],
],
],
];
// Test migrations are not altered when classic node migrations is in use.
$tests[0]['type'] = NodeMigrateType::NODE_MIGRATE_TYPE_CLASSIC;
$tests[0]['migration_definitions'] = $migrations;
$tests[0]['expected'] = $tests[0]['migration_definitions'];
// Test migrations are altered when complete node migrations is in use.
$tests[1] = $tests[0];
$tests[1]['type'] = NodeMigrateType::NODE_MIGRATE_TYPE_COMPLETE;
$tests[1]['expected']['dependencies_altered_if_complete']['migration_dependencies'] = [
'required' => [
'd7_node_complete',
],
'optional' => [
'd7_node_complete',
],
];
return $tests;
}
/**
* Creates data in the source database.
*/
protected function setupDb() {
$this->sourceDatabase->schema()->createTable('system', [
'fields' => [
'name' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
],
'type' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
],
'status' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
],
'schema_version' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '-1',
],
],
]);
$this->sourceDatabase->insert('system')
->fields([
'name',
'type',
'status',
'schema_version',
])
->values([
'name' => 'system',
'type' => 'module',
'status' => '1',
'schema_version' => '7001',
])
->execute();
}
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate;
use Drupal\ban\Plugin\migrate\destination\BlockedIp;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\migrate\Plugin\migrate\destination\ComponentEntityDisplayBase;
use Drupal\migrate\Plugin\migrate\destination\Config;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\shortcut\Plugin\migrate\destination\ShortcutSetUsers;
use Drupal\statistics\Plugin\migrate\destination\NodeCounter;
use Drupal\system\Plugin\migrate\destination\d7\ThemeSettings;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
use Drupal\Tests\migrate_drupal\Traits\CreateMigrationsTrait;
use Drupal\user\Plugin\migrate\destination\UserData;
/**
* Tests that all migrations are tagged as either content or configuration.
*
* @group migrate_drupal
*/
class DestinationCategoryTest extends MigrateDrupalTestBase {
use FileSystemModuleDiscoveryDataProviderTrait;
use CreateMigrationsTrait;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $migrationManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Enable all modules.
self::$modules = array_keys($this->coreModuleListDataProvider());
parent::setUp();
$this->migrationManager = \Drupal::service('plugin.manager.migration');
}
/**
* Tests that all D6 migrations are tagged as either Configuration or Content.
*/
public function testD6Categories(): void {
$migrations = $this->drupal6Migrations();
$this->assertArrayHasKey('d6_node:page', $migrations);
$this->assertCategories($migrations);
}
/**
* Tests that all D7 migrations are tagged as either Configuration or Content.
*/
public function testD7Categories(): void {
$migrations = $this->drupal7Migrations();
$this->assertArrayHasKey('d7_node:page', $migrations);
$this->assertCategories($migrations);
}
/**
* Asserts that all migrations are tagged as either Configuration or Content.
*
* @param \Drupal\migrate\Plugin\MigrationInterface[] $migrations
* The migrations.
*
* @internal
*/
protected function assertCategories(array $migrations): void {
foreach ($migrations as $id => $migration) {
$object_classes = class_parents($migration->getDestinationPlugin());
$object_classes[] = get_class($migration->getDestinationPlugin());
// Ensure that the destination plugin is an instance of at least one of
// the expected classes.
if (in_array('Configuration', $migration->getMigrationTags(), TRUE)) {
$this->assertNotEmpty(array_intersect($object_classes, $this->getConfigurationClasses()), "The migration $id is tagged as Configuration.");
}
elseif (in_array('Content', $migration->getMigrationTags(), TRUE)) {
$this->assertNotEmpty(array_intersect($object_classes, $this->getContentClasses()), "The migration $id is tagged as Content.");
}
else {
$this->fail("The migration $id is not tagged as either 'Content' or 'Configuration'.");
}
}
}
/**
* Get configuration classes.
*
* Configuration migrations should have a destination plugin that is an
* instance of one of the following classes.
*
* @return array
* The configuration class names.
*/
protected function getConfigurationClasses() {
return [
Config::class,
EntityConfigBase::class,
ThemeSettings::class,
ComponentEntityDisplayBase::class,
ShortcutSetUsers::class,
];
}
/**
* Get content classes.
*
* Content migrations should have a destination plugin that is an instance
* of one of the following classes.
*
* @return array
* The content class names.
*/
protected function getContentClasses() {
return [
EntityContentBase::class,
BlockedIp::class,
NodeCounter::class,
UserData::class,
];
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\migrate\source\ContentEntity;
/**
* Tests the constructor of the entity content source plugin.
*
* @group migrate_drupal
*/
class ContentEntityConstructorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_drupal',
'node',
'system',
'user',
];
/**
* Tests the constructor.
*
* @dataProvider providerTestConstructor
*/
public function testConstructor($configuration, $plugin_definition, $exception_class, $expected): void {
$migration = $this->prophesize(MigrationInterface::class)->reveal();
$this->expectException($exception_class);
$this->expectExceptionMessage($expected);
ContentEntity::create($this->container, $configuration, 'content_entity', $plugin_definition, $migration);
}
/**
* Provides data for constructor tests.
*/
public static function providerTestConstructor() {
return [
'entity type missing' => [
[],
['entity_type' => ''],
InvalidPluginDefinitionException::class,
'Missing required "entity_type" definition.',
],
'non content entity' => [
[],
['entity_type' => 'node_type'],
InvalidPluginDefinitionException::class,
'The entity type (node_type) is not supported. The "content_entity" source plugin only supports content entities.',
],
'not bundleable' => [
['bundle' => 'foo'],
['entity_type' => 'user'],
\InvalidArgumentException::class,
'A bundle was provided but the entity type (user) is not bundleable.',
],
'invalid bundle' => [
['bundle' => 'foo'],
['entity_type' => 'node'],
\InvalidArgumentException::class,
'The provided bundle (foo) is not valid for the (node) entity type.',
],
];
}
}

View File

@@ -0,0 +1,474 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\file\Entity\File;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\media\Entity\Media;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\user\Entity\User;
/**
* Tests the entity content source plugin.
*
* @group migrate_drupal
*/
class ContentEntityTest extends KernelTestBase {
use EntityReferenceFieldCreationTrait;
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'migrate',
'migrate_drupal',
'system',
'node',
'taxonomy',
'field',
'file',
'image',
'media',
'media_test_source',
'text',
'filter',
'language',
'content_translation',
];
/**
* The bundle used in this test.
*
* @var string
*/
protected $bundle = 'article';
/**
* The name of the field used in this test.
*
* @var string
*/
protected $fieldName = 'field_entity_reference';
/**
* The vocabulary ID.
*
* @var string
*/
protected $vocabulary = 'fruit';
/**
* The test user.
*
* @var \Drupal\user\Entity\User
*/
protected $user;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('file');
$this->installEntitySchema('media');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
$this->installSchema('user', 'users_data');
$this->installSchema('file', 'file_usage');
$this->installSchema('node', ['node_access']);
$this->installConfig(static::$modules);
ConfigurableLanguage::createFromLangcode('fr')->save();
// Create article content type.
$node_type = NodeType::create(['type' => $this->bundle, 'name' => 'Article']);
$node_type->save();
// Create a vocabulary.
$vocabulary = Vocabulary::create([
'name' => $this->vocabulary,
'description' => $this->vocabulary,
'vid' => $this->vocabulary,
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$vocabulary->save();
// Create a term reference field on node.
$this->createEntityReferenceField(
'node',
$this->bundle,
$this->fieldName,
'Term reference',
'taxonomy_term',
'default',
['target_bundles' => [$this->vocabulary]],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// Create a term reference field on user.
$this->createEntityReferenceField(
'user',
'user',
$this->fieldName,
'Term reference',
'taxonomy_term',
'default',
['target_bundles' => [$this->vocabulary]],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// Create a node, with data in a term reference field, and then add a French
// translation of the node.
$this->user = User::create([
'name' => 'user123',
'uid' => 1,
'mail' => 'example@example.com',
]);
$this->user->save();
// Add the anonymous user so we can test later that it is not provided in a
// source row.
User::create([
'name' => 'anon',
'uid' => 0,
])->save();
$term = Term::create([
'vid' => $this->vocabulary,
'name' => 'Apples',
'uid' => $this->user->id(),
]);
$term->save();
$this->user->set($this->fieldName, $term->id());
$this->user->save();
$node = Node::create([
'type' => $this->bundle,
'title' => 'Apples',
$this->fieldName => $term->id(),
'uid' => $this->user->id(),
]);
$node->save();
$node->addTranslation('fr', [
'title' => 'fr - Apples',
$this->fieldName => $term->id(),
])->save();
$this->migrationPluginManager = $this->container->get('plugin.manager.migration');
}
/**
* Helper to assert IDs structure.
*
* @param \Drupal\migrate\Plugin\MigrateSourceInterface $source
* The source plugin.
* @param array $configuration
* The source plugin configuration (Nope, no getter available).
*
* @internal
*/
protected function assertIds(MigrateSourceInterface $source, array $configuration): void {
$ids = $source->getIds();
[, $entity_type_id] = explode(PluginBase::DERIVATIVE_SEPARATOR, $source->getPluginId());
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
$this->assertArrayHasKey($entity_type->getKey('id'), $ids);
$ids_count_expected = 1;
if ($entity_type->isTranslatable()) {
$ids_count_expected++;
$this->assertArrayHasKey($entity_type->getKey('langcode'), $ids);
}
if ($entity_type->isRevisionable() && $configuration['add_revision_id']) {
$ids_count_expected++;
$this->assertArrayHasKey($entity_type->getKey('revision'), $ids);
}
$this->assertCount($ids_count_expected, $ids);
}
/**
* Tests user source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testUserSource(array $configuration): void {
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:user', $configuration));
$user_source = $migration->getSourcePlugin();
$this->assertSame('users', $user_source->__toString());
if (!$configuration['include_translations']) {
// Confirm that the anonymous user is in the source database but not
// included in the rows returned by the content_entity.
$this->assertNotNull(User::load(0));
$this->assertEquals(1, $user_source->count());
}
$this->assertIds($user_source, $configuration);
$fields = $user_source->fields();
$this->assertArrayHasKey('name', $fields);
$this->assertArrayHasKey('pass', $fields);
$this->assertArrayHasKey('mail', $fields);
$this->assertArrayHasKey('uid', $fields);
$this->assertArrayHasKey('roles', $fields);
$user_source->rewind();
$values = $user_source->current()->getSource();
$this->assertEquals('example@example.com', $values['mail'][0]['value']);
$this->assertEquals('user123', $values['name'][0]['value']);
$this->assertEquals(1, $values['uid']);
$this->assertEquals(1, $values['field_entity_reference'][0]['target_id']);
}
/**
* Tests file source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testFileSource(array $configuration): void {
$file = File::create([
'filename' => 'foo.txt',
'uid' => $this->user->id(),
'uri' => 'public://foo.txt',
]);
$file->save();
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:file', $configuration));
$file_source = $migration->getSourcePlugin();
$this->assertSame('files', $file_source->__toString());
if (!$configuration['include_translations']) {
$this->assertEquals(1, $file_source->count());
}
$this->assertIds($file_source, $configuration);
$fields = $file_source->fields();
$this->assertArrayHasKey('fid', $fields);
$this->assertArrayHasKey('filemime', $fields);
$this->assertArrayHasKey('filename', $fields);
$this->assertArrayHasKey('uid', $fields);
$this->assertArrayHasKey('uri', $fields);
$file_source->rewind();
$values = $file_source->current()->getSource();
$this->assertEquals('text/plain', $values['filemime'][0]['value']);
$this->assertEquals('public://foo.txt', $values['uri'][0]['value']);
$this->assertEquals('foo.txt', $values['filename'][0]['value']);
$this->assertEquals(1, $values['fid']);
}
/**
* Tests node source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testNodeSource(array $configuration): void {
$configuration += ['bundle' => $this->bundle];
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:node', $configuration));
$node_source = $migration->getSourcePlugin();
$this->assertSame('content items', $node_source->__toString());
$this->assertIds($node_source, $configuration);
$fields = $node_source->fields();
$this->assertArrayHasKey('nid', $fields);
$this->assertArrayHasKey('vid', $fields);
$this->assertArrayHasKey('title', $fields);
$this->assertArrayHasKey('uid', $fields);
$this->assertArrayHasKey('sticky', $fields);
$node_source->rewind();
$values = $node_source->current()->getSource();
$this->assertEquals($this->bundle, $values['type'][0]['target_id']);
$this->assertEquals(1, $values['nid']);
if ($configuration['add_revision_id']) {
$this->assertEquals(1, $values['vid']);
}
else {
$this->assertEquals([['value' => '1']], $values['vid']);
}
$this->assertEquals('en', $values['langcode']);
$this->assertEquals(1, $values['status'][0]['value']);
$this->assertEquals('Apples', $values['title'][0]['value']);
$this->assertEquals(1, $values['default_langcode'][0]['value']);
$this->assertEquals(1, $values['field_entity_reference'][0]['target_id']);
if ($configuration['include_translations']) {
$node_source->next();
$values = $node_source->current()->getSource();
$this->assertEquals($this->bundle, $values['type'][0]['target_id']);
$this->assertEquals(1, $values['nid']);
if ($configuration['add_revision_id']) {
$this->assertEquals(1, $values['vid']);
}
else {
$this->assertEquals([0 => ['value' => 1]], $values['vid']);
}
$this->assertEquals('fr', $values['langcode']);
$this->assertEquals(1, $values['status'][0]['value']);
$this->assertEquals('fr - Apples', $values['title'][0]['value']);
$this->assertEquals(0, $values['default_langcode'][0]['value']);
$this->assertEquals(1, $values['field_entity_reference'][0]['target_id']);
}
}
/**
* Tests media source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testMediaSource(array $configuration): void {
$values = [
'id' => 'image',
'label' => 'Image',
'source' => 'test',
'new_revision' => FALSE,
];
$media_type = $this->createMediaType('test', $values);
$media = Media::create([
'name' => 'Foo media',
'uid' => $this->user->id(),
'bundle' => $media_type->id(),
]);
$media->save();
$configuration += [
'bundle' => 'image',
];
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:media', $configuration));
$media_source = $migration->getSourcePlugin();
$this->assertSame('media items', $media_source->__toString());
if (!$configuration['include_translations']) {
$this->assertEquals(1, $media_source->count());
}
$this->assertIds($media_source, $configuration);
$fields = $media_source->fields();
$this->assertArrayHasKey('bundle', $fields);
$this->assertArrayHasKey('mid', $fields);
$this->assertArrayHasKey('vid', $fields);
$this->assertArrayHasKey('name', $fields);
$this->assertArrayHasKey('status', $fields);
$media_source->rewind();
$values = $media_source->current()->getSource();
$this->assertEquals(1, $values['mid']);
if ($configuration['add_revision_id']) {
$this->assertEquals(1, $values['vid']);
}
else {
$this->assertEquals([['value' => 1]], $values['vid']);
}
$this->assertEquals('Foo media', $values['name'][0]['value']);
$this->assertNull($values['thumbnail'][0]['title']);
$this->assertEquals(1, $values['uid'][0]['target_id']);
$this->assertEquals('image', $values['bundle'][0]['target_id']);
}
/**
* Tests term source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testTermSource(array $configuration): void {
$term2 = Term::create([
'vid' => $this->vocabulary,
'name' => 'Granny Smith',
'uid' => $this->user->id(),
'parent' => 1,
]);
$term2->save();
$configuration += [
'bundle' => $this->vocabulary,
];
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:taxonomy_term', $configuration));
$term_source = $migration->getSourcePlugin();
$this->assertSame('taxonomy terms', $term_source->__toString());
if (!$configuration['include_translations']) {
$this->assertEquals(2, $term_source->count());
}
$this->assertIds($term_source, $configuration);
$fields = $term_source->fields();
$this->assertArrayHasKey('vid', $fields);
$this->assertArrayHasKey('revision_id', $fields);
$this->assertArrayHasKey('tid', $fields);
$this->assertArrayHasKey('name', $fields);
$term_source->rewind();
$values = $term_source->current()->getSource();
$this->assertEquals($this->vocabulary, $values['vid'][0]['target_id']);
$this->assertEquals(1, $values['tid']);
$this->assertEquals('Apples', $values['name'][0]['value']);
$this->assertSame([['target_id' => '0']], $values['parent']);
$term_source->next();
$values = $term_source->current()->getSource();
$this->assertEquals($this->vocabulary, $values['vid'][0]['target_id']);
$this->assertEquals(2, $values['tid']);
$this->assertEquals('Granny Smith', $values['name'][0]['value']);
$this->assertSame([['target_id' => '1']], $values['parent']);
}
/**
* Data provider for several test methods.
*
* @see \Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source\ContentEntityTest::testUserSource
* @see \Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source\ContentEntityTest::testFileSource
* @see \Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source\ContentEntityTest::testNodeSource
* @see \Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source\ContentEntityTest::testMediaSource
* @see \Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source\ContentEntityTest::testTermSource
*/
public static function migrationConfigurationProvider() {
$data = [];
foreach ([FALSE, TRUE] as $include_translations) {
foreach ([FALSE, TRUE] as $add_revision_id) {
$configuration = [
'include_translations' => $include_translations,
'add_revision_id' => $add_revision_id,
];
// Add an array key for this data set.
$data[http_build_query($configuration)] = [$configuration];
}
}
return $data;
}
/**
* Get a migration definition.
*
* @param string $plugin_id
* The plugin id.
* @param array $configuration
* The plugin configuration.
*
* @return array
* The definition.
*/
protected function migrationDefinition($plugin_id, array $configuration = []) {
return [
'source' => [
'plugin' => $plugin_id,
] + $configuration,
'process' => [],
'destination' => [
'plugin' => 'null',
],
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
// cspell:ignore multirow
/**
* Tests the variable multirow source plugin.
*
* @covers \Drupal\migrate_drupal\Plugin\migrate\source\VariableMultiRow
*
* @group migrate_drupal
*/
class VariableMultiRowTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['variable'] = [
['name' => 'foo', 'value' => 'i:1;'],
['name' => 'bar', 'value' => 'b:0;'],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'name' => 'foo',
'value' => 1,
],
[
'name' => 'bar',
'value' => FALSE,
],
];
// The expected count.
$tests[0]['expected_count'] = NULL;
// The source plugin configuration.
$tests[0]['configuration']['variables'] = [
'foo',
'bar',
];
return $tests;
}
}

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the variable source plugin.
*
* @covers \Drupal\migrate_drupal\Plugin\migrate\source\Variable
*
* @group migrate_drupal
*/
class VariableTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['variable'] = [
['name' => 'foo', 'value' => 'i:1;'],
['name' => 'bar', 'value' => 'b:0;'],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'id' => 'foo',
'foo' => 1,
'bar' => FALSE,
],
];
// The expected count.
$tests[0]['expected_count'] = 1;
// The source plugin configuration.
$tests[0]['configuration']['variables'] = [
'foo',
'bar',
];
// Tests getting one of two variables.
$tests[1]['source_data']['variable'] = [
['name' => 'foo', 'value' => 'i:1;'],
['name' => 'bar', 'value' => 'b:0;'],
];
$tests[1]['expected_data'] = [
[
'id' => 'foo',
'foo' => 1,
],
];
$tests[1]['expected_count'] = 1;
$tests[1]['configuration']['variables'] = [
'foo',
'bar0',
];
// Tests requesting mis-spelled variable names. If none of the required
// variables are available, this plugin still returns a single row.
$tests[2]['source_data']['variable'] = [
['name' => 'foo', 'value' => 'i:1;'],
['name' => 'bar', 'value' => 'b:0;'],
];
$tests[2]['expected_data'] = [
[
'id' => 'foo0',
],
];
$tests[2]['expected_count'] = 1;
$tests[2]['configuration']['variables'] = [
'foo0',
'bar0',
];
$source_data = [
'variable' => [
['name' => 'foo', 'value' => 'i:1;'],
['name' => 'bar', 'value' => 'b:0;'],
['name' => 'baz', 'value' => 's:6:"foobar";'],
],
];
// Test cases with only 'variables_no_row_if_missing' configuration.
$variables_no_row_if_missing_tests = [
'Two required variables, all of them are available' => [
'source_data' => $source_data,
'expected_data' => [
[
'id' => 'foo',
'foo' => 1,
'bar' => FALSE,
],
],
'expected_count' => 1,
'configuration' => [
'variables_no_row_if_missing' => [
'foo',
'bar',
],
],
],
'Two required variables, only one is available' => [
'source_data' => $source_data,
'expected_data' => [],
'expected_count' => 0,
'configuration' => [
'variables_no_row_if_missing' => [
'foo',
'bar0',
],
],
],
'One required and available variable' => [
'source_data' => $source_data,
'expected_data' => [
[
'id' => 'baz',
'baz' => 'foobar',
],
],
'expected_count' => 1,
'configuration' => [
'variables_no_row_if_missing' => [
'baz',
],
],
],
'One required, but missing variable' => [
'source_data' => $source_data,
'expected_data' => [],
'expected_count' => 0,
'configuration' => [
'variables_no_row_if_missing' => [
'bar0',
],
],
],
// Test cases with both 'variables' and 'variables_no_row_if_missing'
// configuration.
'One optional and two required variables, all of them are available' => [
'source_data' => $source_data,
'expected_data' => [
[
'id' => 'foo',
'foo' => 1,
'bar' => FALSE,
'baz' => 'foobar',
],
],
'expected_count' => 1,
'configuration' => [
'variables' => ['foo'],
'variables_no_row_if_missing' => ['bar', 'baz'],
],
],
'One optional and two required variables, only one required is available' => [
'source_data' => $source_data,
'expected_data' => [],
'expected_count' => 0,
'configuration' => [
'variables' => ['foo'],
'variables_no_row_if_missing' => ['bar', 'foobar'],
],
],
'Two optional and one required and available variable, every optional is missing' => [
'source_data' => $source_data,
'expected_data' => [
[
'id' => 'qux',
'bar' => FALSE,
],
],
'expected_count' => 1,
'configuration' => [
'variables' => ['qux', 'waldo'],
'variables_no_row_if_missing' => ['bar'],
],
],
'Two available optional and a required, but missing variable' => [
'source_data' => $source_data,
'expected_data' => [],
'expected_count' => 0,
'configuration' => [
'variables' => ['baz', 'foo'],
'variables_no_row_if_missing' => [
'foo_bar_baz',
],
],
],
];
return $tests + $variables_no_row_if_missing_tests;
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source\d6;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the variable source plugin.
*
* @covers \Drupal\migrate_drupal\Plugin\migrate\source\d6\VariableTranslation
*
* @group migrate_drupal
*/
class VariableTranslationTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['i18n_variable'] = [
[
'name' => 'site_slogan',
'language' => 'fr',
'value' => 's:23:"fr - migrate is awesome";',
],
[
'name' => 'site_name',
'language' => 'fr',
'value' => 's:14:"fr - site name";',
],
[
'name' => 'site_slogan',
'language' => 'mi',
'value' => 's:23:"mi - migrate is awesome";',
],
[
'name' => 'site_name',
'language' => 'mi',
'value' => 's:14:"mi - site name";',
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'language' => 'fr',
'site_slogan' => 'fr - migrate is awesome',
'site_name' => 'fr - site name',
],
[
'language' => 'mi',
'site_slogan' => 'mi - migrate is awesome',
'site_name' => 'mi - site name',
],
];
// The expected count.
$tests[0]['expected_count'] = NULL;
// The migration configuration.
$tests[0]['configuration']['variables'] = [
'site_slogan',
'site_name',
];
return $tests;
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the variable source plugin.
*
* @covers \Drupal\migrate_drupal\Plugin\migrate\source\d7\VariableTranslation
*
* @group migrate_drupal
*/
class VariableTranslationTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['variable_store'] = [
[
'realm' => 'language',
'realm_key' => 'fr',
'name' => 'site_slogan',
'value' => 'fr - site slogan',
'serialized' => '0',
],
[
'realm' => 'language',
'realm_key' => 'fr',
'name' => 'user_mail_status_blocked_subject',
'value' => 'fr - BEGONE!',
'serialized' => '0',
],
[
'realm' => 'language',
'realm_key' => 'is',
'name' => 'site_slogan',
'value' => 's:16:"is - site slogan";',
'serialized' => '1',
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'language' => 'fr',
'site_slogan' => 'fr - site slogan',
'user_mail_status_blocked_subject' => 'fr - BEGONE!',
],
[
'language' => 'is',
'site_slogan' => 'is - site slogan',
],
];
// The expected count.
$tests[0]['expected_count'] = NULL;
// The migration configuration.
$tests[0]['configuration']['variables'] = [
'site_slogan',
'user_mail_status_blocked_subject',
];
return $tests;
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\Plugin\migrate\source\d8;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the config source plugin.
*
* @covers \Drupal\migrate_drupal\Plugin\migrate\source\d8\Config
* @group migrate_drupal
*/
class ConfigTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$data = [];
// The source database tables.
$data[0]['source_data'] = [
'config' => [
[
'collection' => 'language.af',
'name' => 'user.settings',
'data' => 'a:1:{s:9:"anonymous";s:14:"af - Anonymous";}',
],
[
'collection' => '',
'name' => 'user.settings',
'data' => 'a:1:{s:9:"anonymous";s:9:"Anonymous";}',
],
[
'collection' => 'language.de',
'name' => 'user.settings',
'data' => 'a:1:{s:9:"anonymous";s:14:"de - Anonymous";}',
],
[
'collection' => 'language.af',
'name' => 'bar',
'data' => 'b:0;',
],
],
];
// The expected results.
$data[0]['expected_data'] = [
[
'collection' => 'language.af',
'name' => 'user.settings',
'data' => [
'anonymous' => 'af - Anonymous',
],
],
[
'collection' => 'language.af',
'name' => 'bar',
'data' => FALSE,
],
];
$data[0]['expected_count'] = NULL;
$data[0]['configuration'] = [
'names' => [
'user.settings',
'bar',
],
'collections' => [
'language.af',
],
];
// Test with name and no collection in configuration.
$data[1]['source_data'] = $data[0]['source_data'];
$data[1]['expected_data'] = [
[
'collection' => 'language.af',
'name' => 'bar',
'data' => FALSE,
],
];
$data[1]['expected_count'] = NULL;
$data[1]['configuration'] = [
'names' => [
'bar',
],
];
// Test with collection and no name in configuration.
$data[2]['source_data'] = $data[0]['source_data'];
$data[2]['expected_data'] = [
[
'collection' => 'language.de',
'name' => 'user.settings',
'data' => [
'anonymous' => 'de - Anonymous',
],
],
];
$data[2]['expected_count'] = NULL;
$data[2]['configuration'] = [
'collections' => [
'language.de',
],
];
return $data;
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel;
use Drupal\Component\Discovery\YamlDiscovery;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\migrate_drupal\MigrationConfigurationTrait;
/**
* Tests that core modules have a migrate_drupal.yml file as needed.
*
* Checks that each module that requires a migrate_drupal.yml has the file.
* Because more that one migrate_drupal.yml file may have the same entry the
* ValidateMigrationStateTest, which validates the file contents, is not able
* to determine that all the required files exits.
*
* @group migrate_drupal
*/
class StateFileExistsTest extends MigrateDrupalTestBase {
use FileSystemModuleDiscoveryDataProviderTrait;
use MigrationConfigurationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
// Test migrations states.
'migrate_state_finished_test',
'migrate_state_not_finished_test',
];
/**
* Modules that should have a migrate_drupal.yml file.
*
* @var array
*/
protected $stateFileRequired = [
'ban',
'block',
'block_content',
// @todo Remove book in https://www.drupal.org/project/drupal/issues/3376101
'book',
'comment',
'config_translation',
'contact',
'content_translation',
'datetime',
'dblog',
'field',
'file',
'filter',
// @todo Remove forum in https://www.drupal.org/project/drupal/issues/3261653
'forum',
'image',
'language',
'link',
'locale',
'menu_link_content',
'migrate_state_finished_test',
'migrate_state_not_finished_test',
'menu_ui',
'migrate_drupal',
'node',
'options',
'path',
'responsive_image',
'search',
'shortcut',
// @todo Remove statistics in https://www.drupal.org/project/drupal/issues/3341092
'statistics',
'syslog',
'system',
'taxonomy',
'telephone',
'text',
// @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
'tracker',
'update',
'user',
];
/**
* Tests that the migrate_drupal.yml files exist as needed.
*/
public function testMigrationState(): void {
// Install all available modules.
$module_handler = $this->container->get('module_handler');
$all_modules = $this->coreModuleListDataProvider();
$modules_enabled = $module_handler->getModuleList();
$modules_to_enable = array_keys(array_diff_key($all_modules, $modules_enabled));
$this->enableModules($modules_to_enable);
// Modules with a migrate_drupal.yml file.
$has_state_file = (new YamlDiscovery('migrate_drupal', array_map(function ($value) {
return $value . '/migrations/state';
}, $module_handler->getModuleDirectories())))->findAll();
foreach ($this->stateFileRequired as $module) {
$this->assertArrayHasKey($module, $has_state_file, sprintf("Module '%s' should have a migrate_drupal.yml file", $module));
}
$this->assertSameSize($this->stateFileRequired, $has_state_file);
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\user\Entity\User;
use Prophecy\Argument;
/**
* @group migrate_drupal
*/
class EntityContentBaseTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_overwrite_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a field on the user entity so that we can test nested property
// overwrites.
// @see static::testOverwriteSelectedNestedProperty()
FieldStorageConfig::create([
'field_name' => 'signature',
'entity_type' => 'user',
'type' => 'text_long',
])->save();
FieldConfig::create([
'field_name' => 'signature',
'entity_type' => 'user',
'bundle' => 'user',
])->save();
User::create([
'uid' => 2,
'name' => 'Ford Prefect',
'mail' => 'ford.prefect@localhost',
'signature' => [
[
'value' => 'Bring a towel.',
'format' => 'filtered_html',
],
],
'init' => 'proto@zo.an',
])->save();
$this->executeMigrations(['d6_filter_format', 'd6_user_role']);
}
/**
* Tests overwriting all mapped properties in the destination entity.
*
* This is the default behavior.
*/
public function testOverwriteAllMappedProperties(): void {
$this->executeMigration('d6_user');
/** @var \Drupal\user\UserInterface $account */
$account = User::load(2);
$this->assertSame('john.doe', $account->label());
$this->assertSame('john.doe@example.com', $account->getEmail());
$this->assertSame('doe@example.com', $account->getInitialEmail());
}
/**
* Tests overwriting selected properties in the destination entity.
*
* The selected properties are specified in the destination configuration.
*/
public function testOverwriteProperties(): void {
// Execute the migration in migrate_overwrite_test, which documents how
// property overwrites work.
$this->executeMigration('users');
/** @var \Drupal\user\UserInterface $account */
$account = User::load(2);
$this->assertSame('john.doe', $account->label());
$this->assertSame('john.doe@example.com', $account->getEmail());
$this->assertSame('The answer is 42.', $account->signature->value);
// This value is not overwritten because it's not listed in
// overwrite_properties.
$this->assertSame('proto@zo.an', $account->getInitialEmail());
}
/**
* Tests that translation destination fails for untranslatable entities.
*/
public function testUntranslatable(): void {
$this->enableModules(['language_test']);
$this->installEntitySchema('no_language_entity_test');
/** @var MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([
'source' => [
'plugin' => 'embedded_data',
'ids' => ['id' => ['type' => 'integer']],
'data_rows' => [['id' => 1]],
],
'process' => [
'id' => 'id',
],
'destination' => [
'plugin' => 'entity:no_language_entity_test',
'translations' => TRUE,
],
]);
$message = $this->prophesize(MigrateMessageInterface::class);
// Match the expected message. Can't use default argument types, because
// we need to convert to string from TranslatableMarkup.
$argument = Argument::that(function ($msg) {
return str_contains((string) $msg, htmlentities('The "no_language_entity_test" entity type does not support translations.'));
});
$message->display($argument, Argument::any())
->shouldBeCalled();
$executable = new MigrateExecutable($migration, $message->reveal());
$executable->import();
}
}

View File

@@ -0,0 +1,318 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\field\Plugin\migrate\source\d6\FieldInstance;
use Drupal\field_discovery_test\FieldDiscoveryTestClass;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
use Drupal\Tests\migrate_drupal\Traits\FieldDiscoveryTestTrait;
// cspell:ignore filefield imagefield imagelink nodelink nodereference
// cspell:ignore selectlist spamspan userreference
/**
* Tests FieldDiscovery service against Drupal 6.
*
* @group migrate_drupal
* @coversDefaultClass \Drupal\migrate_drupal\FieldDiscovery
*/
class FieldDiscoveryTest extends MigrateDrupal6TestBase {
use FieldDiscoveryTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'menu_ui',
'comment',
'datetime',
'file',
'image',
'link',
'node',
'system',
'taxonomy',
'telephone',
'text',
];
/**
* The Field discovery service.
*
* @var \Drupal\migrate_drupal\FieldDiscoveryInterface
*/
protected $fieldDiscovery;
/**
* The field plugin manager.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
*/
protected $fieldPluginManager;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* The logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['node']);
$this->executeMigration('d6_node_type');
$this->executeMigration('d6_field');
$this->executeMigration('d6_field_instance');
$this->fieldDiscovery = $this->container->get('migrate_drupal.field_discovery');
$this->migrationPluginManager = $this->container->get('plugin.manager.migration');
$this->fieldPluginManager = $this->container->get('plugin.manager.migrate.field');
$this->logger = $this->container->get('logger.channel.migrate_drupal');
}
/**
* Tests the addAllFieldProcesses method.
*
* @covers ::addAllFieldProcesses
*/
public function testAddAllFieldProcesses(): void {
$expected_process_keys = [
'field_commander',
'field_company',
'field_company_2',
'field_company_3',
'field_company_4',
'field_sync',
'field_multivalue',
'field_test_text_single_checkbox',
'field_reference',
'field_reference_2',
'field_test',
'field_test_date',
'field_test_datestamp',
'field_test_datetime',
'field_test_decimal_radio_buttons',
'field_test_email',
'field_test_exclude_unset',
'field_test_filefield',
'field_test_float_single_checkbox',
'field_test_four',
'field_test_identical1',
'field_test_identical2',
'field_test_imagefield',
'field_test_integer_selectlist',
'field_test_link',
'field_test_phone',
'field_test_string_selectlist',
'field_test_text_single_checkbox2',
'field_test_three',
'field_test_two',
];
$this->assertFieldProcessKeys($this->fieldDiscovery, $this->migrationPluginManager, FieldDiscoveryInterface::DRUPAL_6, $expected_process_keys);
}
/**
* Tests the addAllFieldProcesses method for field migrations.
*
* @covers ::addAllFieldProcesses
* @dataProvider addAllFieldProcessesAltersData
*/
public function testAddAllFieldProcessesAlters($field_plugin_method, $expected_process): void {
$this->assertFieldProcess($this->fieldDiscovery, $this->migrationPluginManager, FieldDiscoveryInterface::DRUPAL_6, $field_plugin_method, $expected_process);
}
/**
* Provides data for testAddAllFieldProcessesAlters.
*
* @return array
* The data.
*/
public static function addAllFieldProcessesAltersData() {
return [
'Field Formatter' => [
'field_plugin_method' => 'alterFieldFormatterMigration',
'expected_process' => [
'options/type' => [
0 => [
'map' => [
'email' => [
'email_formatter_default' => 'email_mailto',
'email_formatter_contact' => 'basic_string',
'email_formatter_plain' => 'basic_string',
'email_formatter_spamspan' => 'basic_string',
'email_default' => 'email_mailto',
'email_contact' => 'basic_string',
'email_plain' => 'basic_string',
'email_spamspan' => 'basic_string',
],
'text' => [
'default' => 'text_default',
'trimmed' => 'text_trimmed',
'plain' => 'basic_string',
],
'datetime' => [
'date_default' => 'datetime_default',
'format_interval' => 'datetime_time_ago',
'date_plain' => 'datetime_plain',
],
'filefield' => [
'default' => 'file_default',
'url_plain' => 'file_url_plain',
'path_plain' => 'file_url_plain',
'image_plain' => 'image',
'image_nodelink' => 'image',
'image_imagelink' => 'image',
],
'link' => [
'default' => 'link',
'plain' => 'link',
'absolute' => 'link',
'title_plain' => 'link',
'url' => 'link',
'short' => 'link',
'label' => 'link',
'separate' => 'link_separate',
],
],
],
],
],
],
'Field Widget' => [
'field_plugin_method' => 'alterFieldWidgetMigration',
'expected_process' => [
'options/type' => [
'type' => [
'map' => [
'userreference_select' => 'options_select',
'userreference_buttons' => 'options_buttons',
'userreference_autocomplete' => 'entity_reference_autocomplete_tags',
'nodereference_select' => 'options_select',
'nodereference_buttons' => 'options_buttons',
'nodereference_autocomplete' => 'entity_reference_autocomplete_tags',
'email_textfield' => 'email_default',
'text_textfield' => 'text_textfield',
'date' => 'datetime_default',
'datetime' => 'datetime_default',
'datestamp' => 'datetime_timestamp',
'filefield_widget' => 'file_generic',
'link' => 'link_default',
],
],
],
],
],
];
}
/**
* Tests the addFields method.
*
* @covers ::addAllFieldProcesses
*/
public function testAddFields(): void {
$this->migrateFields();
$field_discovery = $this->container->get('migrate_drupal.field_discovery');
$migration_plugin_manager = $this->container->get('plugin.manager.migration');
$definition = [
'migration_tags' => ['Drupal 6'],
];
$migration = $migration_plugin_manager->createStubMigration($definition);
$field_discovery->addBundleFieldProcesses($migration, 'node', 'test_planet');
$actual_process = $migration->getProcess();
$expected_process = [
'field_multivalue' => [
0 => [
'plugin' => 'get',
'source' => 'field_multivalue',
],
],
'field_test_text_single_checkbox' => [
0 => [
'plugin' => 'sub_process',
'source' => 'field_test_text_single_checkbox',
'process' => [
'value' => 'value',
'format' => [
0 => [
'plugin' => 'static_map',
'bypass' => TRUE,
'source' => 'format',
'map' => [
0 => NULL,
],
],
1 => [
'plugin' => 'skip_on_empty',
'method' => 'process',
],
2 => [
'plugin' => 'migration_lookup',
'migration' => [
0 => 'd6_filter_format',
1 => 'd7_filter_format',
],
'source' => 'format',
],
],
],
],
],
];
$this->assertEquals($expected_process, $actual_process);
}
/**
* Tests the getAllFields method.
*
* @covers ::getAllFields
*/
public function testGetAllFields(): void {
$field_discovery_test = new FieldDiscoveryTestClass($this->fieldPluginManager, $this->migrationPluginManager, $this->logger);
$actual_fields = $field_discovery_test->getAllFields('6');
$actual_node_types = array_keys($actual_fields['node']);
sort($actual_node_types);
$this->assertSame(['node'], array_keys($actual_fields));
$this->assertSame(['employee', 'page', 'story', 'test_page', 'test_planet'], $actual_node_types);
$this->assertCount(25, $actual_fields['node']['story']);
foreach ($actual_fields['node'] as $bundle => $fields) {
foreach ($fields as $field_name => $field_info) {
$this->assertArrayHasKey('type', $field_info);
$this->assertCount(22, $field_info);
$this->assertEquals($bundle, $field_info['type_name']);
}
}
}
/**
* Tests the getSourcePlugin method.
*
* @covers ::getSourcePlugin
*/
public function testGetSourcePlugin(): void {
$this->assertSourcePlugin('6', FieldInstance::class, [
'requirements_met' => TRUE,
'id' => 'd6_field_instance',
'source_module' => 'content',
'class' => 'Drupal\\field\\Plugin\\migrate\\source\\d6\\FieldInstance',
'provider' => [
0 => 'field',
1 => 'migrate_drupal',
2 => 'migrate',
4 => 'core',
],
]);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\node\Entity\Node;
use Drupal\Tests\node\Kernel\Migrate\d6\MigrateNodeTestBase;
/**
* Tests follow-up migrations.
*
* @group migrate_drupal
*/
class FollowUpMigrationsTest extends MigrateNodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'language',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'language',
'd6_language_content_settings',
'd6_node',
'd6_node_translation',
]);
}
/**
* Tests entity reference translations.
*/
public function testEntityReferenceTranslations(): void {
// Test the entity reference field before the follow-up migrations.
$node = Node::load(10);
$this->assertSame('13', $node->get('field_reference')->target_id);
$this->assertSame('13', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('20', $translation->get('field_reference')->target_id);
$this->assertSame('20', $translation->get('field_reference_2')->target_id);
$node = Node::load(12)->getTranslation('en');
$this->assertSame('10', $node->get('field_reference')->target_id);
$this->assertSame('10', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('11', $translation->get('field_reference')->target_id);
$this->assertSame('11', $translation->get('field_reference_2')->target_id);
// Run the follow-up migrations.
$migration_plugin_manager = $this->container->get('plugin.manager.migration');
$migration_plugin_manager->clearCachedDefinitions();
$follow_up_migrations = $migration_plugin_manager->createInstances('d6_entity_reference_translation');
$this->executeMigrations(array_keys($follow_up_migrations));
// Test the entity reference field after the follow-up migrations.
$node = Node::load(10);
$this->assertSame('12', $node->get('field_reference')->target_id);
$this->assertSame('12', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('12', $translation->get('field_reference')->target_id);
$this->assertSame('12', $translation->get('field_reference_2')->target_id);
$node = Node::load(12)->getTranslation('en');
$this->assertSame('10', $node->get('field_reference')->target_id);
$this->assertSame('10', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('10', $translation->get('field_reference')->target_id);
$this->assertSame('10', $translation->get('field_reference_2')->target_id);
}
}

View File

@@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\migrate\Audit\AuditResult;
use Drupal\migrate\Audit\IdAuditor;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
// cspell:ignore sourceid
/**
* Tests the migration auditor for ID conflicts.
*
* @group migrate_drupal
*/
class MigrateDrupal6AuditIdsTest extends MigrateDrupal6TestBase {
use FileSystemModuleDiscoveryDataProviderTrait;
use CreateTestContentEntitiesTrait;
use ContentModerationTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Enable all modules.
self::$modules = array_keys($this->coreModuleListDataProvider());
parent::setUp();
// Install required entity schemas.
$this->installEntitySchemas();
// Install required schemas.
// @todo Remove book in https://www.drupal.org/project/drupal/issues/3376101
$this->installSchema('book', ['book']);
$this->installSchema('dblog', ['watchdog']);
// @todo Remove forum in https://www.drupal.org/project/drupal/issues/3261653
$this->installSchema('forum', ['forum_index']);
$this->installSchema('node', ['node_access']);
$this->installSchema('search', ['search_dataset']);
// @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
$this->installSchema('tracker', ['tracker_node', 'tracker_user']);
// Enable content moderation for nodes of type page.
$this->installEntitySchema('content_moderation_state');
$this->installConfig('content_moderation');
NodeType::create([
'type' => 'page',
'name' => 'Page',
])->save();
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
$workflow->save();
}
/**
* Tests multiple migrations to the same destination with no ID conflicts.
*/
public function testMultipleMigrationWithoutIdConflicts(): void {
// Create a node of type page.
$node = Node::create(['type' => 'page', 'title' => 'foo']);
$node->moderation_state->value = 'published';
$node->save();
// Insert data in the d6_node:page migration mapping table to simulate a
// previously migrated node.
$id_map = $this->getMigration('d6_node:page')->getIdMap();
$table_name = $id_map->mapTableName();
$id_map->getDatabase()->insert($table_name)
->fields([
'source_ids_hash' => 1,
'sourceid1' => 1,
'destid1' => 1,
])
->execute();
// Audit the IDs of the d6_node migrations for the page & article node type.
// There should be no conflicts since the highest destination ID should be
// equal to the highest migrated ID, as found in the aggregated mapping
// tables of the two node migrations.
$migrations = [
$this->getMigration('d6_node:page'),
$this->getMigration('d6_node:article'),
];
$results = (new IdAuditor())->auditMultiple($migrations);
/** @var \Drupal\migrate\Audit\AuditResult $result */
foreach ($results as $result) {
$this->assertInstanceOf(AuditResult::class, $result);
$this->assertTrue($result->passed());
}
}
/**
* Tests all migrations with no ID conflicts.
*/
public function testAllMigrationsWithNoIdConflicts(): void {
$migrations = $this->container
->get('plugin.manager.migration')
->createInstancesByTag('Drupal 6');
// Audit all Drupal 6 migrations that support it. There should be no
// conflicts since no content has been created.
$results = (new IdAuditor())->auditMultiple($migrations);
/** @var \Drupal\migrate\Audit\AuditResult $result */
foreach ($results as $result) {
$this->assertInstanceOf(AuditResult::class, $result);
$this->assertTrue($result->passed());
}
}
/**
* Tests all migrations with ID conflicts.
*/
public function testAllMigrationsWithIdConflicts(): void {
// Get all Drupal 6 migrations.
$migrations = $this->container
->get('plugin.manager.migration')
->createInstancesByTag('Drupal 6');
// Create content.
$this->createContent();
// Audit the IDs of all migrations. There should be conflicts since content
// has been created.
$conflicts = array_map(
function (AuditResult $result) {
return $result->passed() ? NULL : $result->getMigration()->getBaseId();
},
(new IdAuditor())->auditMultiple($migrations)
);
$expected = [
'd6_comment',
'd6_custom_block',
'd6_file',
'd6_menu_links',
'd6_node',
'd6_node_complete',
'd6_node_revision',
'd6_taxonomy_term',
'd6_term_node_revision',
'd6_user',
'node_translation_menu_links',
];
$this->assertEmpty(array_diff(array_filter($conflicts), $expected));
}
/**
* Tests draft revisions ID conflicts.
*/
public function testDraftRevisionIdConflicts(): void {
// Create a published node of type page.
$node = Node::create(['type' => 'page', 'title' => 'foo']);
$node->moderation_state->value = 'published';
$node->save();
// Create a draft revision.
$node->moderation_state->value = 'draft';
$node->setNewRevision(TRUE);
$node->save();
// Insert data in the d6_node_revision:page migration mapping table to
// simulate a previously migrated node revision.
$id_map = $this->getMigration('d6_node_revision:page')->getIdMap();
$table_name = $id_map->mapTableName();
$id_map->getDatabase()->insert($table_name)
->fields([
'source_ids_hash' => 1,
'sourceid1' => 1,
'destid1' => 1,
])
->execute();
// Audit the IDs of the d6_node_revision migration. There should be
// conflicts since a draft revision has been created.
/** @var \Drupal\migrate\Audit\AuditResult $result */
$result = (new IdAuditor())->audit($this->getMigration('d6_node_revision:page'));
$this->assertInstanceOf(AuditResult::class, $result);
$this->assertFalse($result->passed());
}
/**
* Tests ID conflicts for inaccessible nodes.
*/
public function testNodeGrantsIdConflicts(): void {
// Enable the node_test module to restrict access to page nodes.
$this->enableModules(['node_test']);
// Create a published node of type page.
$node = Node::create(['type' => 'page', 'title' => 'foo']);
$node->moderation_state->value = 'published';
$node->save();
// Audit the IDs of the d6_node migration. There should be conflicts
// even though the new node is not accessible.
/** @var \Drupal\migrate\Audit\AuditResult $result */
$result = (new IdAuditor())->audit($this->getMigration('d6_node:page'));
$this->assertInstanceOf(AuditResult::class, $result);
$this->assertFalse($result->passed());
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\migrate_drupal\NodeMigrateType;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
use Drupal\Tests\migrate_drupal\Traits\NodeMigrateTypeTestTrait;
/**
* Base class for Drupal 6 migration tests.
*/
abstract class MigrateDrupal6TestBase extends MigrateDrupalTestBase {
use NodeMigrateTypeTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'datetime',
'filter',
'image',
'link',
'node',
'options',
'telephone',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Add a node classic migrate table to the destination site so that tests
// run by default with the classic node migrations.
$this->makeNodeMigrateMapTable(NodeMigrateType::NODE_MIGRATE_TYPE_CLASSIC, '6');
$this->loadFixture($this->getFixtureFilePath());
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../fixtures/drupal6.php';
}
/**
* Executes all user migrations.
*
* @param bool $include_pictures
* If TRUE, migrates user pictures.
*/
protected function migrateUsers($include_pictures = TRUE) {
$this->executeMigrations(['d6_filter_format', 'd6_user_role']);
if ($include_pictures) {
$this->installEntitySchema('file');
$this->executeMigrations([
'd6_file',
'd6_user_picture_file',
'user_picture_field',
'user_picture_field_instance',
'user_picture_entity_display',
'user_picture_entity_form_display',
]);
}
$this->executeMigration('d6_user');
}
/**
* Migrates node types.
*/
protected function migrateContentTypes() {
$this->installConfig(['node']);
$this->executeMigration('d6_node_type');
}
/**
* Executes all field migrations.
*/
protected function migrateFields() {
$this->migrateContentTypes();
$this->executeMigrations([
'd6_field',
'd6_field_instance',
'd6_field_instance_widget_settings',
'd6_view_modes',
'd6_field_formatter_settings',
'd6_upload_field',
'd6_upload_field_instance',
]);
}
/**
* Executes all content migrations.
*
* @param array $include
* Extra things to include as part of the migrations. Values may be
* 'revisions' or 'translations'.
*/
protected function migrateContent(array $include = []) {
if (in_array('translations', $include)) {
$this->executeMigrations(['language']);
}
$this->migrateUsers(FALSE);
$this->migrateFields();
$this->installEntitySchema('node');
$this->executeMigrations(['d6_node_settings', 'd6_node']);
if (in_array('translations', $include)) {
$this->executeMigrations(['d6_node_translation']);
}
if (in_array('revisions', $include)) {
$this->executeMigrations(['d6_node_revision']);
}
}
/**
* Executes all taxonomy migrations.
*/
protected function migrateTaxonomy() {
$this->migrateContentTypes();
$this->installEntitySchema('taxonomy_term');
$this->executeMigrations([
'd6_taxonomy_vocabulary',
'd6_vocabulary_field',
'd6_vocabulary_field_instance',
'd6_vocabulary_entity_display',
'd6_vocabulary_entity_form_display',
'd6_taxonomy_term',
]);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
/**
* Tests the getProcess() method of all Drupal 6 migrations.
*
* @group migrate_drupal
*/
class MigrationProcessTest extends MigrateDrupal6TestBase {
use FileSystemModuleDiscoveryDataProviderTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
self::$modules = array_keys($this->coreModuleListDataProvider());
parent::setUp();
}
/**
* Tests that calling getProcess() on a migration does not throw an exception.
*
* @throws \Exception
*/
public function testGetProcess(): void {
/** @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migration');
$migrations = $plugin_manager->createInstancesByTag('Drupal 6');
foreach ($migrations as $migration) {
try {
$process = $migration->getProcess();
}
catch (\Exception $e) {
$this->fail(sprintf("Migration %s process failed with error: %s", $migration->label(), $e->getMessage()));
}
$this->assertNotNull($process);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\Tests\migrate_drupal\Traits\ValidateMigrationStateTestTrait;
/**
* Tests the migration state information in module.migrate_drupal.yml.
*
* @group migrate_drupal
*/
class ValidateMigrationStateTest extends MigrateDrupal6TestBase {
use ValidateMigrationStateTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
// Test migrations states.
'migrate_state_finished_test',
'migrate_state_not_finished_test',
];
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\migrate\Exception\RequirementsException;
/**
* Tests check requirements for variable translation source plugin.
*
* @group migrate_drupal
*/
class VariableTranslationCheckRequirementsTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_translation'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->sourceDatabase->schema()->dropTable('i18n_variable');
}
/**
* Tests exception in thrown when the i18n_variable table does not exist.
*/
public function testCheckRequirements(): void {
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage("Source database table 'i18n_variable' does not exist");
$this->getMigration('d6_system_maintenance_translation')
->getSourcePlugin()
->checkRequirements();
}
}

View File

@@ -0,0 +1,366 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d7;
use Drupal\comment\Entity\CommentType;
use Drupal\field\Plugin\migrate\source\d7\FieldInstance;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\migrate_drupal\Traits\FieldDiscoveryTestTrait;
use Drupal\field_discovery_test\FieldDiscoveryTestClass;
// cspell:ignore filefield imagelink entityreference nodelink spamspan
/**
* Test FieldDiscovery Service against Drupal 7.
*
* @group migrate_drupal
* @coversDefaultClass \Drupal\migrate_drupal\FieldDiscovery
*/
class FieldDiscoveryTest extends MigrateDrupal7TestBase {
use FieldDiscoveryTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'datetime',
'datetime_range',
'file',
'image',
'link',
'node',
'system',
'taxonomy',
'telephone',
'text',
];
/**
* The Field discovery service.
*
* @var \Drupal\migrate_drupal\FieldDiscoveryInterface
*/
protected $fieldDiscovery;
/**
* The field plugin manager.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
*/
protected $fieldPluginManager;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* The logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(static::$modules);
$node_types = [
'page' => 'comment_node_page',
'article' => 'comment_node_article',
'blog' => 'comment_node_blog',
'book' => 'comment_node_book',
'et' => 'comment_node_et',
'forum' => 'comment_forum',
'test_content_type' => 'comment_node_test_content_type',
'a_thirty_two_character_type_name' => 'a_thirty_two_character_type_name',
];
foreach ($node_types as $node_type => $comment_type) {
NodeType::create([
'type' => $node_type,
'name' => $this->randomString(),
])->save();
CommentType::create([
'id' => $comment_type,
'label' => $this->randomString(),
'target_entity_type_id' => 'node',
])->save();
}
Vocabulary::create(['vid' => 'test_vocabulary', 'name' => 'Test'])->save();
$this->executeMigrations([
'd7_field',
'd7_comment_type',
'd7_taxonomy_vocabulary',
'd7_field_instance',
]);
$this->fieldDiscovery = $this->container->get('migrate_drupal.field_discovery');
$this->migrationPluginManager = $this->container->get('plugin.manager.migration');
$this->fieldPluginManager = $this->container->get('plugin.manager.migrate.field');
$this->logger = $this->container->get('logger.channel.migrate_drupal');
}
/**
* Tests the addAllFieldProcesses method.
*
* @covers ::addAllFieldProcesses
*/
public function testAddAllFieldProcesses(): void {
$expected_process_keys = [
'comment_body',
'field_integer',
'body',
'field_text_plain',
'field_text_filtered',
'field_text_plain_filtered',
'field_text_long_plain',
'field_text_long_filtered',
'field_text_long_plain_filtered',
'field_text_sum_plain',
'field_text_sum_filtered',
'field_text_sum_plain_filtered',
'field_tags',
'field_image',
'field_link',
'field_reference',
'field_reference_2',
'taxonomy_forums',
'field_boolean',
'field_email',
'field_phone',
'field_date',
'field_date_with_end_time',
'field_file',
'field_float',
'field_images',
'field_text_list',
'field_integer_list',
'field_long_text',
'field_term_reference',
'field_text',
'field_node_entityreference',
'field_user_entityreference',
'field_term_entityreference',
'field_node_reference',
'field_user_reference',
'field_private_file',
'field_datetime_without_time',
'field_date_without_time',
'field_float_list',
'field_training',
'field_sector',
'field_chancellor',
];
$this->assertFieldProcessKeys($this->fieldDiscovery, $this->migrationPluginManager, '7', $expected_process_keys);
}
/**
* Tests the addAllFieldProcesses method for field migrations.
*
* @covers ::addAllFieldProcesses
* @dataProvider addAllFieldProcessesAltersData
*/
public function testAddAllFieldProcessesAlters($field_plugin_method, $expected_process): void {
$this->assertFieldProcess($this->fieldDiscovery, $this->migrationPluginManager, FieldDiscoveryInterface::DRUPAL_7, $field_plugin_method, $expected_process);
}
/**
* Provides data for testAddAllFieldProcessesAlters.
*
* @return array
* The data.
*/
public static function addAllFieldProcessesAltersData() {
return [
'Field Instance' => [
'field_plugin_method' => 'alterFieldInstanceMigration',
'expected_process' => [
'settings/title' => [
0 => [
'plugin' => 'static_map',
'source' => 'settings/title',
'bypass' => TRUE,
'map' => [
'disabled' => 0,
'optional' => 1,
'required' => 2,
],
],
],
],
],
'Field Formatter' => [
'field_plugin_method' => 'alterFieldFormatterMigration',
'expected_process' => [
'options/type' => [
0 => [
'map' => [
'taxonomy_term_reference' => [
'taxonomy_term_reference_link' => 'entity_reference_label',
'taxonomy_term_reference_plain' => 'entity_reference_label',
'taxonomy_term_reference_rss_category' => 'entity_reference_label',
'i18n_taxonomy_term_reference_link' => 'entity_reference_label',
'i18n_taxonomy_term_reference_plain' => 'entity_reference_label',
'entityreference_entity_view' => 'entity_reference_entity_view',
],
'link_field' => [
'link_default' => 'link',
'link_title_plain' => 'link',
'link_host' => 'link',
'link_url' => 'link',
'link_plain' => 'link',
'link_absolute' => 'link',
'link_domain' => 'link',
'link_no_protocol' => 'link',
'link_short' => 'link',
'link_label' => 'link',
'link_separate' => 'link_separate',
],
'entityreference' => [
'entityreference_label' => 'entity_reference_label',
'entityreference_entity_id' => 'entity_reference_entity_id',
'entityreference_entity_view' => 'entity_reference_entity_view',
],
'node_reference' => [
'node_reference_default' => 'entity_reference_label',
'node_reference_plain' => 'entity_reference_label',
'node_reference_nid' => 'entity_reference_entity_id',
'node_reference_node' => 'entity_reference_entity_view',
'node_reference_path' => 'entity_reference_label',
],
'user_reference' => [
'user_reference_default' => 'entity_reference_label',
'user_reference_plain' => 'entity_reference_label',
'user_reference_uid' => 'entity_reference_entity_id',
'user_reference_user' => 'entity_reference_entity_view',
'user_reference_path' => 'entity_reference_label',
],
'file' => [
'default' => 'file_default',
'url_plain' => 'file_url_plain',
'path_plain' => 'file_url_plain',
'image_plain' => 'image',
'image_nodelink' => 'image',
'image_imagelink' => 'image',
],
'datetime' => [
'date_default' => 'datetime_default',
'format_interval' => 'datetime_time_ago',
'date_plain' => 'datetime_plain',
],
'email' => [
'email_formatter_default' => 'email_mailto',
'email_formatter_contact' => 'basic_string',
'email_formatter_plain' => 'basic_string',
'email_formatter_spamspan' => 'basic_string',
'email_default' => 'email_mailto',
'email_contact' => 'basic_string',
'email_plain' => 'basic_string',
'email_spamspan' => 'basic_string',
],
'phone' => [
'phone' => 'basic_string',
],
'telephone' => [
'text_plain' => 'string',
'telephone_link' => 'telephone_link',
],
],
],
],
],
],
'Field Widget' => [
'field_plugin_method' => 'alterFieldWidgetMigration',
'expected_process' => [
'options/type' => [
'type' => [
'map' => [
'd7_text' => 'd7_text_default',
'number_default' => 'number_default_default',
'taxonomy_term_reference' => 'taxonomy_term_reference_default',
'image' => 'image_default',
'image_miw' => 'image_image',
'link_field' => 'link_default',
'entityreference' => 'entityreference_default',
'node_reference_select' => 'options_select',
'node_reference_buttons' => 'options_buttons',
'node_reference_autocomplete' => 'entity_reference_autocomplete_tags',
'user_reference_select' => 'options_select',
'user_reference_buttons' => 'options_buttons',
'user_reference_autocomplete' => 'entity_reference_autocomplete_tags',
'list' => 'list_default',
'file_mfw' => 'file_generic',
'filefield_widget' => 'file_generic',
'date' => 'datetime_default',
'datetime' => 'datetime_default',
'datestamp' => 'datetime_timestamp',
'email_textfield' => 'email_default',
'phone' => 'phone_default',
],
],
],
],
],
];
}
/**
* Tests the getAllFields method.
*
* @covers ::getAllFields
*/
public function testGetAllFields(): void {
$field_discovery_test = new FieldDiscoveryTestClass($this->fieldPluginManager, $this->migrationPluginManager, $this->logger);
$actual_fields = $field_discovery_test->getAllFields('7');
$this->assertSame(['comment', 'node', 'user', 'taxonomy_term'], array_keys($actual_fields));
$this->assertArrayHasKey('test_vocabulary', $actual_fields['taxonomy_term']);
$this->assertArrayHasKey('user', $actual_fields['user']);
$this->assertArrayHasKey('test_content_type', $actual_fields['node']);
$this->assertCount(8, $actual_fields['node']);
$this->assertCount(8, $actual_fields['comment']);
$this->assertCount(23, $actual_fields['node']['test_content_type']);
foreach ($actual_fields as $entity_type_id => $bundles) {
foreach ($bundles as $bundle => $fields) {
foreach ($fields as $field_name => $field_info) {
$this->assertArrayHasKey('field_definition', $field_info);
$this->assertEquals($entity_type_id, $field_info['entity_type']);
$this->assertEquals($bundle, $field_info['bundle']);
}
}
}
}
/**
* Tests the getSourcePlugin method.
*
* @covers ::getSourcePlugin
*/
public function testGetSourcePlugin(): void {
$this->assertSourcePlugin('7', FieldInstance::class, [
'requirements_met' => TRUE,
'id' => 'd7_field_instance',
'source_module' => 'field',
'class' => 'Drupal\\field\\Plugin\\migrate\\source\\d7\\FieldInstance',
'provider' => [
0 => 'field',
1 => 'migrate_drupal',
2 => 'migrate',
4 => 'core',
],
]);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d7;
use Drupal\node\Entity\Node;
use Drupal\Tests\file\Kernel\Migrate\d7\FileMigrationSetupTrait;
use Drupal\user\Entity\User;
/**
* Tests follow-up migrations.
*
* @group migrate_drupal
*/
class FollowUpMigrationsTest extends MigrateDrupal7TestBase {
use FileMigrationSetupTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'comment',
'datetime',
'datetime_range',
'image',
'language',
'link',
'menu_ui',
'node',
'taxonomy',
'telephone',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->fileMigrationSetup();
$this->installEntitySchema('comment');
$this->installSchema('node', ['node_access']);
$this->migrateFields();
$this->migrateUsers();
$this->executeMigrations([
'language',
'd7_language_content_settings',
'd7_taxonomy_vocabulary',
]);
}
/**
* {@inheritdoc}
*/
protected function getFileMigrationInfo() {
return [
'path' => 'public://sites/default/files/cube.jpeg',
'size' => 3620,
'base_path' => 'public://',
'plugin_id' => 'd7_file',
];
}
/**
* Tests entity reference translations.
*
* @dataProvider providerTestEntityReferenceTranslations
*/
public function testEntityReferenceTranslations($node_migrations): void {
$this->executeMigrations($node_migrations);
// Test the entity reference field before the follow-up migrations.
$node = Node::load(2);
$this->assertSame('5', $node->get('field_reference')->target_id);
$this->assertSame('6', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('is');
$this->assertSame('4', $translation->get('field_reference')->target_id);
$this->assertSame('4', $translation->get('field_reference_2')->target_id);
$node = Node::load(4);
$this->assertSame('3', $node->get('field_reference')->target_id);
$this->assertSame('3', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('en');
$this->assertSame('2', $translation->get('field_reference')->target_id);
$this->assertSame('2', $translation->get('field_reference_2')->target_id);
$user = User::load(2);
$this->assertSame('3', $user->get('field_reference')->target_id);
// Run the follow-up migrations.
$migration_plugin_manager = $this->container->get('plugin.manager.migration');
$migration_plugin_manager->clearCachedDefinitions();
$follow_up_migrations = $migration_plugin_manager->createInstances('d7_entity_reference_translation');
$this->executeMigrations(array_keys($follow_up_migrations));
// Test the entity reference field after the follow-up migrations.
$node = Node::load(2);
$this->assertSame('4', $node->get('field_reference')->target_id);
$this->assertSame('6', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('is');
$this->assertSame('4', $translation->get('field_reference')->target_id);
$this->assertSame('4', $translation->get('field_reference_2')->target_id);
$node = Node::load(4);
$this->assertSame('2', $node->get('field_reference')->target_id);
$this->assertSame('2', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('en');
$this->assertSame('2', $translation->get('field_reference')->target_id);
$this->assertSame('2', $translation->get('field_reference_2')->target_id);
$user = User::load(2);
$this->assertSame('2', $user->get('field_reference')->target_id);
}
/**
* Data provider for testEntityReferenceTranslations().
*/
public static function providerTestEntityReferenceTranslations() {
return [
[
['d7_node', 'd7_node_translation'],
],
[
['d7_node_complete'],
],
];
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d7;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\migrate\Audit\AuditResult;
use Drupal\migrate\Audit\IdAuditor;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
/**
* Tests the migration auditor for ID conflicts.
*
* @group migrate_drupal
*/
class MigrateDrupal7AuditIdsTest extends MigrateDrupal7TestBase {
use FileSystemModuleDiscoveryDataProviderTrait;
use CreateTestContentEntitiesTrait;
use ContentModerationTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Enable all modules.
self::$modules = array_keys($this->coreModuleListDataProvider());
parent::setUp();
// Install required entity schemas.
$this->installEntitySchemas();
// Install required schemas.
// @todo Remove book in https://www.drupal.org/project/drupal/issues/3376101
$this->installSchema('book', ['book']);
$this->installSchema('dblog', ['watchdog']);
// @todo Remove forum in https://www.drupal.org/project/drupal/issues/3261653
$this->installSchema('forum', ['forum_index']);
$this->installSchema('node', ['node_access']);
$this->installSchema('search', ['search_dataset']);
// @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
$this->installSchema('tracker', ['tracker_node', 'tracker_user']);
// Enable content moderation for nodes of type page.
$this->installEntitySchema('content_moderation_state');
$this->installConfig('content_moderation');
NodeType::create([
'type' => 'page',
'name' => 'Page',
])->save();
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
$workflow->save();
}
/**
* Tests multiple migrations to the same destination with no ID conflicts.
*/
public function testMultipleMigrationWithoutIdConflicts(): void {
// Create a node of type page.
$node = Node::create(['type' => 'page', 'title' => 'foo']);
$node->moderation_state->value = 'published';
$node->save();
// Insert data in the d7_node:page migration mapping table to simulate a
// previously migrated node.
$id_map = $this->getMigration('d7_node:page')->getIdMap();
$table_name = $id_map->mapTableName();
$id_map->getDatabase()->insert($table_name)
->fields([
'source_ids_hash' => 1,
'sourceid1' => 1,
'destid1' => 1,
])
->execute();
// Audit the IDs of the d7_node migrations for the page & article node type.
// There should be no conflicts since the highest destination ID should be
// equal to the highest migrated ID, as found in the aggregated mapping
// tables of the two node migrations.
$migrations = [
$this->getMigration('d7_node:page'),
$this->getMigration('d7_node:article'),
];
$results = (new IdAuditor())->auditMultiple($migrations);
/** @var \Drupal\migrate\Audit\AuditResult $result */
foreach ($results as $result) {
$this->assertInstanceOf(AuditResult::class, $result);
$this->assertTrue($result->passed());
}
}
/**
* Tests all migrations with no ID conflicts.
*/
public function testAllMigrationsWithNoIdConflicts(): void {
$migrations = $this->container
->get('plugin.manager.migration')
->createInstancesByTag('Drupal 7');
// Audit the IDs of all Drupal 7 migrations. There should be no conflicts
// since no content has been created.
$results = (new IdAuditor())->auditMultiple($migrations);
/** @var \Drupal\migrate\Audit\AuditResult $result */
foreach ($results as $result) {
$this->assertInstanceOf(AuditResult::class, $result);
$this->assertTrue($result->passed());
}
}
/**
* Tests all migrations with ID conflicts.
*/
public function testAllMigrationsWithIdConflicts(): void {
$migrations = $this->container
->get('plugin.manager.migration')
->createInstancesByTag('Drupal 7');
// Create content.
$this->createContent();
// Audit the IDs of all Drupal 7 migrations. There should be conflicts since
// content has been created.
$conflicts = array_map(
function (AuditResult $result) {
return $result->passed() ? NULL : $result->getMigration()->getBaseId();
},
(new IdAuditor())->auditMultiple($migrations)
);
$expected = [
'd7_comment',
'd7_custom_block',
'd7_file',
'd7_file_private',
'd7_menu_links',
'd7_node',
'd7_node_complete',
'd7_node_revision',
'd7_taxonomy_term',
'd7_user',
'node_translation_menu_links',
];
$this->assertEmpty(array_diff(array_filter($conflicts), $expected));
}
/**
* Tests draft revisions ID conflicts.
*/
public function testDraftRevisionIdConflicts(): void {
// Create a published node of type page.
$node = Node::create(['type' => 'page', 'title' => 'foo']);
$node->moderation_state->value = 'published';
$node->save();
// Create a draft revision.
$node->moderation_state->value = 'draft';
$node->setNewRevision(TRUE);
$node->save();
// Insert data in the d7_node_revision:page migration mapping table to
// simulate a previously migrated node revision.
$id_map = $this->getMigration('d7_node_revision:page')->getIdMap();
$table_name = $id_map->mapTableName();
$id_map->getDatabase()->insert($table_name)
->fields([
'source_ids_hash' => 1,
'sourceid1' => 1,
'destid1' => 1,
])
->execute();
// Audit the IDs of the d7_node_revision migration. There should be
// conflicts since a draft revision has been created.
/** @var \Drupal\migrate\Audit\AuditResult $result */
$result = (new IdAuditor())->audit($this->getMigration('d7_node_revision:page'));
$this->assertInstanceOf(AuditResult::class, $result);
$this->assertFalse($result->passed());
}
/**
* Tests ID conflicts for inaccessible nodes.
*/
public function testNodeGrantsIdConflicts(): void {
// Enable the node_test module to restrict access to page nodes.
$this->enableModules(['node_test']);
// Create a published node of type page.
$node = Node::create(['type' => 'page', 'title' => 'foo']);
$node->moderation_state->value = 'published';
$node->save();
// Audit the IDs of the d7_node migration. There should be conflicts
// even though the new node is not accessible.
/** @var \Drupal\migrate\Audit\AuditResult $result */
$result = (new IdAuditor())->audit($this->getMigration('d7_node:page'));
$this->assertInstanceOf(AuditResult::class, $result);
$this->assertFalse($result->passed());
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d7;
use Drupal\migrate_drupal\NodeMigrateType;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
use Drupal\Tests\migrate_drupal\Traits\NodeMigrateTypeTestTrait;
/**
* Base class for Drupal 7 migration tests.
*/
abstract class MigrateDrupal7TestBase extends MigrateDrupalTestBase {
use NodeMigrateTypeTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Add a node classic migrate table to the destination site so that tests
// run by default with the classic node migrations.
$this->makeNodeMigrateMapTable(NodeMigrateType::NODE_MIGRATE_TYPE_CLASSIC, '7');
$this->loadFixture($this->getFixtureFilePath());
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../fixtures/drupal7.php';
}
/**
* Executes all field migrations.
*/
protected function migrateFields() {
$this->executeMigration('d7_field');
$this->migrateContentTypes();
$this->migrateCommentTypes();
$this->executeMigrations(['d7_taxonomy_vocabulary', 'd7_field_instance']);
}
/**
* Executes all user migrations.
*
* @param bool $include_pictures
* (optional) If TRUE, migrates user pictures. Defaults to TRUE.
*/
protected function migrateUsers($include_pictures = TRUE) {
$migrations = ['d7_user_role', 'd7_user'];
if ($include_pictures) {
// Prepare to migrate user pictures as well.
$this->installEntitySchema('file');
$migrations = array_merge([
'user_picture_field',
'user_picture_field_instance',
], $migrations);
}
$this->executeMigrations($migrations);
}
/**
* Migrates node types.
*/
protected function migrateContentTypes() {
$this->installConfig(['node']);
$this->installEntitySchema('node');
$this->executeMigration('d7_node_type');
}
/**
* Migrates comment types.
*/
protected function migrateCommentTypes() {
$this->installConfig(['comment']);
$this->executeMigration('d7_comment_type');
}
/**
* Executes all content migrations.
*
* @param bool $include_revisions
* (optional) If TRUE, migrates node revisions. Defaults to FALSE.
*/
protected function migrateContent($include_revisions = FALSE) {
$this->migrateContentTypes();
$this->migrateCommentTypes();
$this->migrateUsers(FALSE);
// Uses executeMigrations() rather than executeMigration() because the
// former includes all of the migration derivatives, e.g.
// d7_node:article.
$this->executeMigrations(['d7_node']);
if ($include_revisions) {
$this->executeMigrations(['d7_node_revision']);
}
}
/**
* Executes all taxonomy term migrations.
*/
protected function migrateTaxonomyTerms() {
$this->installEntitySchema('taxonomy_term');
$this->migrateFields();
// Uses executeMigrations() rather than executeMigration() because the
// former includes all of the migration derivatives, e.g.
// d7_taxonomy_term:tags.
$this->executeMigrations(['d7_taxonomy_term']);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d7;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
/**
* Tests the getProcess() method of all Drupal 7 migrations.
*
* @group migrate_drupal
*/
class MigrationProcessTest extends MigrateDrupal7TestBase {
use FileSystemModuleDiscoveryDataProviderTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
self::$modules = array_keys($this->coreModuleListDataProvider());
parent::setUp();
}
/**
* Tests that calling getProcess() on a migration does not throw an exception.
*
* @throws \Exception
*/
public function testGetProcess(): void {
/** @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migration');
$migrations = $plugin_manager->createInstancesByTag('Drupal 7');
foreach ($migrations as $migration) {
try {
$process = $migration->getProcess();
}
catch (\Exception $e) {
$this->fail(sprintf("Migration %s process failed with error: %s", $migration->label(), $e->getMessage()));
}
$this->assertNotNull($process);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\d7;
use Drupal\Tests\migrate_drupal\Traits\ValidateMigrationStateTestTrait;
/**
* Tests the migration state information in module.migrate_drupal.yml.
*
* @group migrate_drupal
*/
class ValidateMigrationStateTest extends MigrateDrupal7TestBase {
use ValidateMigrationStateTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
// Test migrations states.
'migrate_state_finished_test',
'migrate_state_not_finished_test',
];
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Kernel\dependencies;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Ensure the consistency among the dependencies for migrate.
*
* @group migrate_drupal
*/
class MigrateDependenciesTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['comment'];
/**
* Tests that the order is correct when loading several migrations.
*/
public function testMigrationDependenciesOrder(): void {
$migration_items = ['d6_comment', 'd6_filter_format', 'd6_node:page'];
/** @var \Drupal\migrate\Plugin\RequirementsInterface[] $migrations */
$migrations = $this->container->get('plugin.manager.migration')->createInstances($migration_items);
$expected_order = ['d6_filter_format', 'd6_node:page', 'd6_comment'];
$this->assertSame(array_keys($migrations), $expected_order);
// Migration dependencies for comment include dependencies for node
// migration as well. checkRequirements does not include migrations with
// no rows in the exception, so node types with no content aren't included
// in the list.
try {
$migrations['d6_comment']->checkRequirements();
$this->fail("The requirements check failed to throw a RequirementsException");
}
catch (RequirementsException $e) {
$this->assertEquals('Missing migrations d6_comment_type, d6_user, d6_comment_entity_display, d6_node_type, d6_comment_entity_form_display, d6_node_settings, d6_filter_format, d6_node:company, d6_node:employee, d6_node:forum, d6_node:page, d6_node:story, d6_node:test_planet.', $e->getMessage());
}
catch (\Exception $e) {
$this->fail("The requirements check threw an exception, but it was not the expected RequirementsException");
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Traits;
trait CreateMigrationsTrait {
/**
* Create instances of all Drupal 6 migrations.
*
* @return \Drupal\migrate\Plugin\MigrationInterface[]
* The migrations
*/
public function drupal6Migrations() {
$dirs = \Drupal::service('module_handler')->getModuleDirectories();
$migrate_drupal_directory = $dirs['migrate_drupal'];
$this->loadFixture("$migrate_drupal_directory/tests/fixtures/drupal6.php");
return \Drupal::service('plugin.manager.migration')->createInstancesByTag('Drupal 6');
}
/**
* Create instances of all Drupal 7 migrations.
*
* @return \Drupal\migrate\Plugin\MigrationInterface[]
* The migrations
*/
public function drupal7Migrations() {
$dirs = \Drupal::service('module_handler')->getModuleDirectories();
$migrate_drupal_directory = $dirs['migrate_drupal'];
$this->loadFixture("$migrate_drupal_directory/tests/fixtures/drupal7.php");
return \Drupal::service('plugin.manager.migration')->createInstancesByTag('Drupal 7');
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Traits;
/**
* Provides helper methods for creating test content.
*/
trait CreateTestContentEntitiesTrait {
/**
* Gets required modules.
*
* @return array
*/
protected function getRequiredModules() {
return [
'block_content',
'comment',
'field',
'file',
'link',
'menu_link_content',
'migrate_drupal',
'node',
'options',
'system',
'taxonomy',
'text',
'user',
];
}
/**
* Install required entity schemas.
*/
protected function installEntitySchemas() {
$this->installEntitySchema('block_content');
$this->installEntitySchema('comment');
$this->installEntitySchema('file');
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('node');
$this->installEntitySchema('path_alias');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
}
/**
* Create several pieces of generic content.
*/
protected function createContent() {
$entity_type_manager = \Drupal::entityTypeManager();
// Create a block content.
if ($entity_type_manager->hasDefinition('block_content')) {
$block = $entity_type_manager->getStorage('block_content')->create([
'info' => 'block',
'type' => 'block',
]);
$block->save();
}
// Create a node.
if ($entity_type_manager->hasDefinition('node')) {
$node = $entity_type_manager->getStorage('node')->create([
'type' => 'page',
'title' => 'page',
]);
$node->save();
// Create a comment.
if ($entity_type_manager->hasDefinition('comment')) {
$comment = $entity_type_manager->getStorage('comment')->create([
'comment_type' => 'comment',
'field_name' => 'comment',
'entity_type' => 'node',
'entity_id' => $node->id(),
]);
$comment->save();
}
}
// Create a file.
if ($entity_type_manager->hasDefinition('file')) {
$file = $entity_type_manager->getStorage('file')->create([
'uri' => 'public://example.txt',
]);
$file->save();
}
// Create a menu link.
if ($entity_type_manager->hasDefinition('menu_link_content')) {
$menu_link = $entity_type_manager->getStorage('menu_link_content')->create([
'title' => 'menu link',
'link' => ['uri' => 'http://www.example.com'],
'menu_name' => 'tools',
]);
$menu_link->save();
}
// Create a taxonomy term.
if ($entity_type_manager->hasDefinition('taxonomy_term')) {
$term = $entity_type_manager->getStorage('taxonomy_term')->create([
'name' => 'term',
'vid' => 'term',
]);
$term->save();
}
// Create a user.
if ($entity_type_manager->hasDefinition('user')) {
$user = $entity_type_manager->getStorage('user')->create([
'name' => 'user',
'mail' => 'user@example.com',
]);
$user->save();
}
}
/**
* Create several pieces of generic content.
*/
protected function createContentPostUpgrade() {
$entity_type_manager = \Drupal::entityTypeManager();
// Create a block content.
if ($entity_type_manager->hasDefinition('block_content')) {
$block = $entity_type_manager->getStorage('block_content')->create([
'info' => 'Post upgrade block',
'type' => 'block',
]);
$block->save();
}
// Create a node.
if ($entity_type_manager->hasDefinition('node')) {
$node = $entity_type_manager->getStorage('node')->create([
'type' => 'page',
'title' => 'Post upgrade page',
]);
$node->save();
// Create a comment.
if ($entity_type_manager->hasDefinition('comment')) {
$comment = $entity_type_manager->getStorage('comment')->create([
'comment_type' => 'comment',
'field_name' => 'comment',
'entity_type' => 'node',
'entity_id' => $node->id(),
]);
$comment->save();
}
}
// Create a file.
if ($entity_type_manager->hasDefinition('file')) {
$file = $entity_type_manager->getStorage('file')->create([
'uri' => 'public://post_upgrade_example.txt',
]);
$file->save();
}
// Create a menu link.
if ($entity_type_manager->hasDefinition('menu_link_content')) {
$menu_link = $entity_type_manager->getStorage('menu_link_content')->create([
'title' => 'post upgrade menu link',
'link' => ['uri' => 'http://www.example.com'],
'menu_name' => 'tools',
]);
$menu_link->save();
}
// Create a taxonomy term.
if ($entity_type_manager->hasDefinition('taxonomy_term')) {
$term = $entity_type_manager->getStorage('taxonomy_term')->create([
'name' => 'post upgrade term',
'vid' => 'term',
]);
$term->save();
}
// Create a user.
if ($entity_type_manager->hasDefinition('user')) {
$user = $entity_type_manager->getStorage('user')->create([
'name' => 'universe',
'mail' => 'universe@example.com',
]);
$user->save();
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Traits;
use Drupal\field_discovery_test\FieldDiscoveryTestClass;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
/**
* Helper functions to test field discovery.
*/
trait FieldDiscoveryTestTrait {
/**
* Asserts the field discovery returns the expected processes.
*
* @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
* The Field Discovery service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager service.
* @param string $core
* The Drupal core version, either '6', or '7'.
* @param string $field_plugin_method
* (optional) The field plugin method to use.
* @param array $expected_process
* (optional) The expected resulting process.
* @param string $entity_type_id
* (optional) The entity type id.
* @param string $bundle
* (optional) The bundle.
*/
public function assertFieldProcess(FieldDiscoveryInterface $field_discovery, MigrationPluginManagerInterface $migration_plugin_manager, $core, $field_plugin_method = NULL, array $expected_process = [], $entity_type_id = NULL, $bundle = NULL) {
$definition = [
'migration_tags' => ['Drupal ' . $core],
'field_plugin_method' => $field_plugin_method,
];
$migration = $migration_plugin_manager->createStubMigration($definition);
if ($bundle) {
$field_discovery->addBundleFieldProcesses($migration, $entity_type_id, $bundle);
}
elseif ($entity_type_id) {
$field_discovery->addEntityFieldProcesses($migration, $entity_type_id);
}
else {
$field_discovery->addAllFieldProcesses($migration);
}
$actual_process = $migration->getProcess();
$this->assertSame($expected_process, $actual_process);
}
/**
* Asserts the field discovery returns the expected processes.
*
* @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
* The Field Discovery service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager service.
* @param string $core
* The Drupal core version, either '6', or '7'.
* @param array $expected_process_keys
* (optional) The expected resulting process_keys.
* @param string $entity_type_id
* (optional) The entity type id.
* @param string $bundle
* (optional) The bundle.
*/
public function assertFieldProcessKeys(FieldDiscoveryInterface $field_discovery, MigrationPluginManagerInterface $migration_plugin_manager, $core, array $expected_process_keys, $entity_type_id = NULL, $bundle = NULL) {
$definition = [
'migration_tags' => ['Drupal ' . $core],
];
$migration = $migration_plugin_manager->createStubMigration($definition);
if ($bundle) {
$field_discovery->addBundleFieldProcesses($migration, $entity_type_id, $bundle);
}
elseif ($entity_type_id) {
$field_discovery->addEntityFieldProcesses($migration, $entity_type_id);
}
else {
$field_discovery->addAllFieldProcesses($migration);
}
$actual_process = $migration->getProcess();
$actual = array_keys($actual_process);
$this->assertSame(sort($expected_process_keys), sort($actual));
}
/**
* Asserts a migrate source plugin.
*
* @param string $core
* The Drupal core version.
* @param string $class
* The expected class of the source plugin.
* @param array $expected_definition
* The expected source plugin definition.
*/
public function assertSourcePlugin($core, $class, array $expected_definition) {
$field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager, $this->migrationPluginManager, $this->logger);
$source = $field_discovery->getSourcePlugin($core);
$this->assertInstanceOf($class, $source);
$this->assertSame($expected_definition, $source->getPluginDefinition());
}
}

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Traits;
use Drupal\migrate_drupal\NodeMigrateType;
// cspell:ignore destid sourceid
/**
* Helper functions to test complete and classic node migrations.
*/
trait NodeMigrateTypeTestTrait {
/**
* The migrate_map table name.
*
* @var string
*/
public $tableName = NULL;
/**
* Gets the numbers of complete and classic node migrate_map tables.
*
* @param string $version
* The source database version.
*
* @return array
* An associative array with the total number of complete and classic
* node migrate_map tables.
*/
protected function nodeMigrateMapTableCount($version) {
$results = [];
$bases = ['node', 'node_complete'];
$tables = \Drupal::database()->schema()
->findTables('migrate_map_d' . $version . '_node%');
foreach ($bases as $base) {
$base_tables = preg_grep('/^migrate_map_d' . $version . '_' . $base . '_{2}.*$/', $tables);
$results[$base] = count($base_tables);
}
return $results;
}
/**
* Remove the node migrate map table.
*
* @param string $type
* The type of node migration, 'complete' or 'classic'.
* @param string $version
* The source database version.
*
* @throws \Exception
*/
protected function removeNodeMigrateMapTable($type, $version) {
$name = $this->getTableName($type, $version);
\Drupal::database()->schema()->dropTable($name);
}
/**
* Gets the migrate_map table name.
*
* @param string $type
* The type of node migration, 'complete' or 'classic'.
* @param string $version
* The source database version.
*
* @return string
* The migrate_map table name.
*/
protected function getTableName($type, $version) {
if (!$this->tableName) {
$content_type = $this->randomMachineName();
$this->tableName = 'migrate_map_d' . $version . '_node_complete__' . $content_type;
if ($type == NodeMigrateType::NODE_MIGRATE_TYPE_CLASSIC) {
$this->tableName = 'migrate_map_d' . $version . '_node__' . $content_type;
}
}
return $this->tableName;
}
/**
* Create a node migrate_map table.
*
* @param string $type
* The type of node migration, 'complete' or 'classic'.
* @param string $version
* The source database version.
*
* @throws \Exception
*/
protected function makeNodeMigrateMapTable($type, $version) {
$name = $this->getTableName($type, $version);
$fields = [
'source_ids_hash' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '64',
],
'sourceid1' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
],
'sourceid2' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
],
'sourceid3' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
],
'destid1' => [
'type' => 'int',
'not null' => FALSE,
'size' => 'normal',
'unsigned' => TRUE,
],
'destid2' => [
'type' => 'int',
'not null' => FALSE,
'size' => 'normal',
'unsigned' => TRUE,
],
'destid3' => [
'type' => 'varchar_ascii',
'not null' => FALSE,
'length' => '12',
],
'source_row_status' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'tiny',
'default' => '0',
'unsigned' => TRUE,
],
'rollback_action' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'tiny',
'default' => '0',
'unsigned' => TRUE,
],
'last_imported' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
'unsigned' => TRUE,
],
'hash' => [
'type' => 'varchar',
'not null' => FALSE,
'length' => '64',
],
];
$values = [
'source_ids_hash' => '123',
'sourceid1' => '4242',
'sourceid2' => '4242',
'sourceid3' => 'en',
'destid1' => '4242',
'destid2' => '4242',
'destid3' => 'en',
'source_row_status' => '1',
'rollback_action' => '1',
'last_imported' => time(),
'hash' => 'abc',
];
$indexes = [
'source' => [
'sourceid1',
'sourceid2',
'sourceid3',
],
];
// Remove keys not used.
if ($type == NodeMigrateType::NODE_MIGRATE_TYPE_CLASSIC) {
$keys = ['sourceid2', 'sourceid3', 'destid2', 'destid3'];
foreach ($keys as $key) {
unset($fields[$key]);
unset($values[$key]);
if (str_contains($key, 'sourceid')) {
$index_key = substr($key, -1) - 1;
unset($indexes['source'][$index_key]);
}
}
}
$connection = \Drupal::database();
$connection->schema()->createTable($name, [
'fields' => $fields,
'primary key' => [
'source_ids_hash',
],
'indexes' => $indexes,
'mysql_character_set' => 'utf8mb4',
]);
$field_names = array_keys($fields);
$connection->insert($name)
->fields($field_names)
->values($values)
->execute();
}
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Traits;
use Drupal\Component\Discovery\YamlDiscovery;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\migrate_drupal\MigrationConfigurationTrait;
use Drupal\migrate_drupal\MigrationState;
/**
* Tests the migration state information in module.migrate_drupal.yml.
*
* This test checks that the discovered upgrade paths, which are based on the
* source_module and destination_module definition matches the declared
* upgrade paths in all the migrate_drupal.yml files.
*
* @group migrate_drupal
*/
trait ValidateMigrationStateTestTrait {
use FileSystemModuleDiscoveryDataProviderTrait;
use MigrationConfigurationTrait;
/**
* Tests the migration information in .migrate_drupal.yml.
*
* Checks that every discovered pair has a corresponding declaration in the
* declared pairs. The alternate check, that each declared pair has a
* corresponding discovered pair is not possible because declarations can be
* made for the two cases where migrations are yet to be written and where
* migrations are not needed.
*/
public function testMigrationState(): void {
// Level separator of destination and source properties.
$separator = ',';
$this->enableAllModules();
$version = (string) $this->getLegacyDrupalVersion($this->sourceDatabase);
// Build an array for each migration keyed by provider. The value is a
// string consisting of the version number, the provider, the source_module
// and the destination module.
$discovered = [];
/** @var \Drupal\migrate\Plugin\MigrationPluginManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migration');
$migrations = $plugin_manager->createInstancesByTag('Drupal ' . $version);
/** @var \Drupal\migrate\Plugin\Migration $migration */
foreach ($migrations as $migration) {
$definition = $migration->getPluginDefinition();
if (is_array($definition['provider'])) {
$provider = reset($definition['provider']);
}
else {
$provider = $definition['provider'];
}
$source_module = $migration->getSourcePlugin()->getSourceModule();
$destination_module = $migration->getDestinationPlugin()
->getDestinationModule();
$discovered[] = implode($separator, [
$provider,
$source_module,
$destination_module,
]);
}
// Add the field migrations.
/** @var \Drupal\migrate\Plugin\MigrationPluginManager $plugin_manager */
$definitions = $this->container->get('plugin.manager.migrate.field')
->getDefinitions();
foreach ($definitions as $key => $definition) {
if (isset($definition['core'][$version])) {
$discovered[] = implode($separator, [
$definition['provider'],
$definition['source_module'],
$definition['destination_module'],
]);
}
}
// Get the declared migration state information from .migrate_drupal.yml
// and build an array of source modules and there migration state. The
// destination is not used yet but can be later for validating the
// source/destination pairs with the actual source/destination pairs in the
// migrate plugins.
$system_info = (new YamlDiscovery('migrate_drupal', array_map(function ($value) {
return $value . '/migrations/state/';
}, \Drupal::moduleHandler()->getModuleDirectories())))->findAll();
$declared = [
MigrationState::FINISHED => [],
MigrationState::NOT_FINISHED => [],
];
foreach ($system_info as $module => $info) {
foreach (array_keys($declared) as $state) {
if (isset($info[$state][$version])) {
foreach ($info[$state][$version] as $source => $destination) {
// Do not add the source module i18nstrings or i18_string. The
// 18n migrations can have up to three source modules but only one
// can be handled in the migration.
if (($source !== 'i18nstrings') && ($source !== 'i18n_string')) {
foreach ((array) $destination as $dest) {
$key = [$module, $source, trim($dest)];
$declared[$state][] = implode($separator, $key);
}
}
}
}
}
}
// Sort and make the array values unique.
sort($declared[MigrationState::FINISHED]);
sort($declared[MigrationState::NOT_FINISHED]);
$declared_unique[MigrationState::FINISHED] = array_unique($declared[MigrationState::FINISHED]);
$declared_unique[MigrationState::NOT_FINISHED] = array_unique($declared[MigrationState::NOT_FINISHED]);
sort($discovered);
$discovered_unique = array_unique($discovered);
// Assert that each discovered migration has a corresponding declaration
// in a migrate_drupal.yml.
foreach ($discovered_unique as $datum) {
$data = str_getcsv($datum);
$in_finished = in_array($datum, $declared_unique[MigrationState::FINISHED]);
$in_not_finished = in_array($datum, $declared_unique[MigrationState::NOT_FINISHED]);
$found = $in_finished || $in_not_finished;
$this->assertTrue($found, sprintf("No migration state found for version '%s' with source_module '%s' and destination_module '%s' declared in module '%s'", $version, $data[1], $data[2], $data[0]));
}
// Remove the declared finished from the discovered, leaving just the not
// finished, if there are any. These should have an entry in the declared
// not finished.
$discovered_not_finished = array_diff($discovered_unique, $declared_unique[MigrationState::FINISHED]);
foreach ($discovered_not_finished as $datum) {
$data = str_getcsv($datum);
$this->assertContains($datum, $declared_unique[MigrationState::NOT_FINISHED], sprintf("No migration found for version '%s' with source_module '%s' and destination_module '%s' declared in module '%s'", $version, $data[1], $data[2], $data[0]));
}
}
/**
* Enable all available modules.
*/
protected function enableAllModules() {
// Install all available modules.
$module_handler = $this->container->get('module_handler');
$modules = $this->coreModuleListDataProvider();
$modules_enabled = $module_handler->getModuleList();
$modules_to_enable = array_keys(array_diff_key($modules, $modules_enabled));
$this->enableModules($modules_to_enable);
return $modules;
}
}

View File

@@ -0,0 +1,361 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Unit;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\field_discovery_test\FieldDiscoveryTestClass;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
use Drupal\Tests\UnitTestCase;
/**
* Tests the FieldDiscovery Class.
*
* @group migrate_drupal
* @coversDefaultClass \Drupal\migrate_drupal\FieldDiscovery
*/
class FieldDiscoveryTest extends UnitTestCase {
/**
* A MigrateFieldPluginManager prophecy.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $fieldPluginManager;
/**
* A MigrationPluginManager prophecy.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $migrationPluginManager;
/**
* A LoggerChannelInterface prophecy.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $logger;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->fieldPluginManager = $this->prophesize(MigrateFieldPluginManagerInterface::class);
$this->migrationPluginManager = $this->prophesize(MigrationPluginManagerInterface::class);
$this->logger = $this->prophesize(LoggerChannelInterface::class);
}
/**
* Tests the protected getEntityFields method.
*
* @param string $entity_type_id
* The entity type ID.
* @param array $expected_fields
* The expected fields.
*
* @covers ::getEntityFields
* @dataProvider getEntityFieldsData
*/
public function testGetEntityFields($entity_type_id, array $expected_fields): void {
$test_data = [
'getAllFields' => [
'7' => $this->getAllFieldData(),
],
];
$field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager->reveal(), $this->migrationPluginManager->reveal(), $this->logger->reveal(), $test_data);
$actual_fields = $field_discovery->getEntityFields('7', $entity_type_id);
$this->assertSame($expected_fields, $actual_fields);
}
/**
* Provides data for testGetEntityFields.
*
* @return array
* The data.
*/
public static function getEntityFieldsData() {
return [
'Node' => [
'entity_type_id' => 'node',
'expected_fields' => [
'content_type_1' => [
'field_1' => ['field_info_key' => 'field_1_data'],
'field_2' => ['field_info_key' => 'field_2_data'],
'field_3' => ['field_info_key' => 'field_3_data'],
],
'content_type_2' => [
'field_1' => ['field_info_key' => 'field_1_data'],
'field_4' => ['field_info_key' => 'field_4_data'],
'field_5' => ['field_info_key' => 'field_5_data'],
],
],
],
'User' => [
'entity_type_id' => 'user',
'expected_fields' => [
'user' => [
'user_field_1' => ['field_info_key' => 'user_field_1_data'],
],
],
],
'Comment' => [
'entity_type_id' => 'comment',
'expected_fields' => [
'comment_node_content_type_1' => [
'comment_field_1' => ['field_info_key' => 'field_1_data'],
'comment_field_2' => ['field_info_key' => 'field_2_data'],
'comment_field_3' => ['field_info_key' => 'field_3_data'],
],
'comment_node_content_type_2' => [
'comment_field_1' => ['field_info_key' => 'field_1_data'],
'comment_field_4' => ['field_info_key' => 'field_4_data'],
'comment_field_5' => ['field_info_key' => 'field_5_data'],
],
],
],
'Non-existent Entity' => [
'entity_type_id' => 'custom_entity',
'expected_fields' => [],
],
];
}
/**
* Tests the protected getEntityFields method.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
* @param array $expected_fields
* The expected fields.
*
* @covers ::getBundleFields
* @dataProvider getBundleFieldsData
*/
public function testGetBundleFields($entity_type_id, $bundle, array $expected_fields): void {
$test_data = [
'getAllFields' => [
'7' => $this->getAllFieldData(),
],
];
$field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager->reveal(), $this->migrationPluginManager->reveal(), $this->logger->reveal(), $test_data);
$actual_fields = $field_discovery->getBundleFields('7', $entity_type_id, $bundle);
$this->assertSame($expected_fields, $actual_fields);
}
/**
* Provides data for testGetBundleFields.
*
* @return array
* The data.
*/
public static function getBundleFieldsData() {
return [
'Node - Content Type 1' => [
'entity_type_id' => 'node',
'bundle' => 'content_type_1',
'expected_fields' => [
'field_1' => ['field_info_key' => 'field_1_data'],
'field_2' => ['field_info_key' => 'field_2_data'],
'field_3' => ['field_info_key' => 'field_3_data'],
],
],
'Node - Content Type 2' => [
'entity_type_id' => 'node',
'bundle' => 'content_type_2',
'expected_fields' => [
'field_1' => ['field_info_key' => 'field_1_data'],
'field_4' => ['field_info_key' => 'field_4_data'],
'field_5' => ['field_info_key' => 'field_5_data'],
],
],
'User' => [
'entity_type_id' => 'user',
'bundle' => 'user',
'expected_fields' => [
'user_field_1' => ['field_info_key' => 'user_field_1_data'],
],
],
'Comment - Content Type 1' => [
'entity_type_id' => 'comment',
'bundle' => 'comment_node_content_type_1',
'expected_fields' => [
'comment_field_1' => ['field_info_key' => 'field_1_data'],
'comment_field_2' => ['field_info_key' => 'field_2_data'],
'comment_field_3' => ['field_info_key' => 'field_3_data'],
],
],
'Comment - Content Type 2' => [
'entity_type_id' => 'comment',
'bundle' => 'comment_node_content_type_2',
'expected_fields' => [
'comment_field_1' => ['field_info_key' => 'field_1_data'],
'comment_field_4' => ['field_info_key' => 'field_4_data'],
'comment_field_5' => ['field_info_key' => 'field_5_data'],
],
],
'Non-existent Entity Type' => [
'entity_type_id' => 'custom_entity',
'bundle' => 'content_type_1',
'expected_fields' => [],
],
'Non-existent Bundle' => [
'entity_type_id' => 'node',
'bundle' => 'content_type_3',
'expected_fields' => [],
],
];
}
/**
* Tests the protected getCoreVersion method.
*
* @param string[] $tags
* The migration tags.
* @param string|bool $expected_result
* The expected return value of the method.
*
* @covers ::getCoreVersion
* @dataProvider getCoreVersionData
*/
public function testGetCoreVersion(array $tags, $expected_result): void {
$migration = $this->prophesize(MigrationInterface::class);
$migration->getMigrationTags()->willReturn($tags);
$field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager->reveal(), $this->migrationPluginManager->reveal(), $this->logger->reveal());
if (!$expected_result) {
$this->expectException(\InvalidArgumentException::class);
}
$actual_result = $field_discovery->getCoreVersion($migration->reveal());
$this->assertEquals($expected_result, $actual_result);
}
/**
* Provides data for testGetCoreVersion()
*
* @return array
* The test data.
*/
public static function getCoreVersionData() {
return [
'Drupal 7' => [
'tags' => ['Drupal 7'],
'expected_result' => '7',
],
'Drupal 6' => [
'tags' => ['Drupal 6'],
'expected_result' => '6',
],
'D7 with others' => [
'tags' => ['Drupal 7', 'Translation', 'Other Tag'],
'expected_result' => '7',
],
'Both (d7 has priority)' => [
'tags' => ['Drupal 6', 'Drupal 7'],
'expected_result' => '7',
],
'Neither' => [
'tags' => ['drupal 6', 'Drupal_6', 'This contains Drupal 7 but is not'],
'expected_result' => FALSE,
],
];
}
/**
* Returns dummy data to test the field getters.
*/
protected function getAllFieldData() {
return [
'node' => [
'content_type_1' => [
'field_1' => ['field_info_key' => 'field_1_data'],
'field_2' => ['field_info_key' => 'field_2_data'],
'field_3' => ['field_info_key' => 'field_3_data'],
],
'content_type_2' => [
'field_1' => ['field_info_key' => 'field_1_data'],
'field_4' => ['field_info_key' => 'field_4_data'],
'field_5' => ['field_info_key' => 'field_5_data'],
],
],
'user' => [
'user' => [
'user_field_1' => ['field_info_key' => 'user_field_1_data'],
],
],
'comment' => [
'comment_node_content_type_1' => [
'comment_field_1' => ['field_info_key' => 'field_1_data'],
'comment_field_2' => ['field_info_key' => 'field_2_data'],
'comment_field_3' => ['field_info_key' => 'field_3_data'],
],
'comment_node_content_type_2' => [
'comment_field_1' => ['field_info_key' => 'field_1_data'],
'comment_field_4' => ['field_info_key' => 'field_4_data'],
'comment_field_5' => ['field_info_key' => 'field_5_data'],
],
],
];
}
/**
* Tests the getFieldInstanceStubMigration method.
*
* @param mixed $core
* The Drupal core version.
* @param array|bool $expected_definition
* The expected migration definition, or false if an exception is expected.
*
* @covers ::getFieldInstanceStubMigrationDefinition
* @dataProvider getFieldInstanceStubMigrationDefinition
*/
public function testGetFieldInstanceStubMigrationDefinition($core, $expected_definition): void {
$field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager->reveal(), $this->migrationPluginManager->reveal(), $this->logger->reveal());
if (!$expected_definition) {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf("Drupal version %s is not supported. Valid values for Drupal core version are '6' and '7'.", $core));
}
$actual_definition = $field_discovery->getFieldInstanceStubMigrationDefinition($core);
$this->assertSame($expected_definition, $actual_definition);
}
/**
* Provides data for testGetFieldInstanceStubMigrationDefinition.
*
* @return array
* The data.
*/
public static function getFieldInstanceStubMigrationDefinition() {
return [
'Drupal 6' => [
'core' => FieldDiscoveryInterface::DRUPAL_6,
'expected_definition' => [
'destination' => ['plugin' => 'null'],
'idMap' => ['plugin' => 'null'],
'source' => [
'ignore_map' => TRUE,
'plugin' => 'd6_field_instance',
],
],
],
'Drupal 7' => [
'core' => FieldDiscoveryInterface::DRUPAL_7,
'expected_definition' => [
'destination' => ['plugin' => 'null'],
'idMap' => ['plugin' => 'null'],
'source' => [
'ignore_map' => TRUE,
'plugin' => 'd7_field_instance',
],
],
],
];
}
}

View File

@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Unit;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\migrate_drupal\Annotation\MigrateField;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManager;
use Drupal\Tests\UnitTestCase;
/**
* Tests the MigrateFieldPluginManager class.
*
* @group migrate_drupal
* @coversDefaultClass \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManager
*/
class MigrateFieldPluginManagerTest extends UnitTestCase {
/**
* Tests the plugin weighting system.
*
* @covers ::getPluginIdFromFieldType
* @covers ::sortDefinitions
* @covers ::findDefinitions
* @dataProvider weightsData
*/
public function testWeights($field_type, $core, $expected_plugin_id): void {
/** @var \Drupal\Core\Cache\CacheBackendInterface $cache */
$cache = $this->prophesize(CacheBackendInterface::class)->reveal();
/** @var \Drupal\Core\Extension\ModuleHandlerInterfaceModuleHandlerInterface $module_handler */
$module_handler = $this->prophesize(ModuleHandlerInterface::class)->reveal();
$discovery = $this->prophesize(AnnotatedClassDiscovery::class);
$discovery->getDefinitions()->willReturn($this->pluginFixtureData());
$manager = new MigrateFieldPluginManagerTestClass('field', new \ArrayObject(), $cache, $module_handler, MigrateField::class, $discovery->reveal());
if (!$expected_plugin_id) {
$this->expectException(PluginNotFoundException::class);
$this->expectExceptionMessage(sprintf("Plugin ID '%s' was not found.", $field_type));
}
$actual_plugin_id = $manager->getPluginIdFromFieldType($field_type, ['core' => $core]);
$this->assertSame($expected_plugin_id, $actual_plugin_id);
}
/**
* Provides data for testWeights().
*
* @return array
* The data.
*/
public static function weightsData() {
return [
'Field 1, D6' => [
'field_type' => 'field_1',
'core' => 6,
'expected_plugin_id' => 'core_replacement_plugin',
],
'Field 2, D6' => [
'field_type' => 'field_2',
'core' => 6,
'expected_plugin_id' => 'field_1',
],
'Field 3, D6' => [
'field_type' => 'field_3',
'core' => 6,
'expected_plugin_id' => 'field_3',
],
'Field 4, D6' => [
'field_type' => 'field_4',
'core' => 6,
'expected_plugin_id' => 'field_4',
],
'Field 5, D6' => [
'field_type' => 'field_5',
'core' => 6,
'expected_plugin_id' => 'alphabetically_second',
],
'Field 1, D7' => [
'field_type' => 'field_1',
'core' => 7,
'expected_plugin_id' => 'core_replacement_plugin',
],
'Field 2, D7' => [
'field_type' => 'field_2',
'core' => 7,
'expected_plugin_id' => FALSE,
],
'Field 3, D7' => [
'field_type' => 'field_3',
'core' => 7,
'expected_plugin_id' => 'field_3',
],
'Field 4, D7' => [
'field_type' => 'field_4',
'core' => 7,
'expected_plugin_id' => 'contrib_override_plugin',
],
'Field 5, D7' => [
'field_type' => 'field_5',
'core' => 7,
'expected_plugin_id' => 'alphabetically_first',
],
];
}
/**
* Returns test plugin data for the test class to use.
*
* @return array
* The test plugin data.
*/
protected function pluginFixtureData() {
return [
// Represents a deprecated core field plugin that applied to field_1
// and field_2 for Drupal 6.
'field_1' => [
'weight' => 99999999,
'core' => [6],
'type_map' => [
'field_1' => 'field_1',
'field_2' => 'field_2',
],
'source_module' => 'system',
'destination_module' => 'system',
],
// Replacement for deprecated plugin for field_1 in Drupal 6 and 7.
// Does not provide replacement for field_2.
'core_replacement_plugin' => [
'weight' => 0,
'core' => [6, 7],
'type_map' => [
'field_1' => 'field_1',
],
'source_module' => 'system',
'destination_module' => 'system',
],
// Represents a core plugin with no type_map, applies to field_3 due to
// plugin id.
'field_3' => [
'core' => [6, 7],
'type_map' => [],
'weight' => 0,
'source_module' => 'system',
'destination_module' => 'system',
],
// Represents a core plugin with no type_map, applies to field_4 due to
// plugin id.
'field_4' => [
'core' => [6, 7],
'type_map' => [],
'weight' => 0,
'source_module' => 'system',
'destination_module' => 'system',
],
// Represents a contrib plugin overriding field_4 for Drupal 7 only.
'contrib_override_plugin' => [
'weight' => -100,
'core' => [7],
'type_map' => [
'field_4' => 'field_4',
],
'source_module' => 'system',
'destination_module' => 'system',
],
// field_5 is served by alphabetically_second in Drupal 6 and
// alphabetically_first and alphabetically_second in Drupal 7. It should
// be served by the alphabetically_first in Drupal 7 regardless of the
// order they appear here.
'alphabetically_second' => [
'weight' => 0,
'core' => [6, 7],
'type_map' => [
'field_5' => 'field_5',
],
'source_module' => 'system',
'destination_module' => 'system',
],
'alphabetically_first' => [
'weight' => 0,
'core' => [7],
'type_map' => [
'field_5' => 'field_5',
],
'source_module' => 'system',
'destination_module' => 'system',
],
];
}
}
/**
* Class to test MigrateFieldPluginManager.
*
* Overrides the constructor to inject a mock discovery class to provide a test
* list of plugins.
*/
class MigrateFieldPluginManagerTestClass extends MigrateFieldPluginManager {
/**
* Constructs a MigratePluginManagerTestClass 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 $annotation
* The annotation class name.
* @param \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery $discovery
* A mock plugin discovery object for the test class to use.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, $annotation, AnnotatedClassDiscovery $discovery) {
parent::__construct($type, $namespaces, $cache_backend, $module_handler, $annotation);
$this->discovery = $discovery;
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Unit;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\migrate_drupal\MigrationConfigurationTrait;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate_drupal\MigrationConfigurationTrait
* @group migrate_drupal
*/
class MigrationConfigurationTraitTest extends UnitTestCase {
/**
* @covers ::getLegacyDrupalVersion
* @dataProvider providerTestGetLegacyDrupalVersion
*/
public function testGetLegacyDrupalVersion($expected_version_string, $schema_version, $exception, $system_table_exists): void {
if ($schema_version) {
$statement = $this->createMock('\Drupal\Core\Database\StatementInterface');
$statement->expects($this->any())
->method('fetchField')
->willReturn($schema_version);
}
$schema = $this->createMock('\Drupal\Core\Database\Schema');
$schema->expects($this->once())
->method('tableExists')
->willReturn($system_table_exists);
$connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
if ($exception) {
$connection->expects($this->any())
->method('query')
->willThrowException($exception);
}
else {
$connection->expects($this->any())
->method('query')
->willReturn($statement);
}
$connection->expects($this->any())
->method('schema')
->willReturn($schema);
$actual_version_string = TestMigrationConfigurationTrait::getLegacyDrupalVersion($connection);
$this->assertSame($expected_version_string, $actual_version_string);
}
/**
* Provides data for testGetLegacyDrupalVersion.
*/
public static function providerTestGetLegacyDrupalVersion() {
return [
'D5' => [
'expected_version_string' => '5',
'schema_version' => '1678',
'exception' => NULL,
'system_table_exists' => TRUE,
],
'D6' => [
'expected_version_string' => '6',
'schema_version' => '6057',
'exception' => NULL,
'system_table_exists' => TRUE,
],
'D7' => [
'expected_version_string' => '7',
'schema_version' => '7065',
'exception' => NULL,
'system_table_exists' => TRUE,
],
'D8' => [
'expected_version_string' => FALSE,
'schema_version' => serialize('8976'),
'exception' => NULL,
'system_table_exists' => FALSE,
],
'D9' => [
'expected_version_string' => FALSE,
'schema_version' => serialize('9270'),
'exception' => NULL,
'system_table_exists' => FALSE,
],
'D10' => [
'expected_version_string' => FALSE,
'schema_version' => serialize('10101'),
'exception' => NULL,
'system_table_exists' => FALSE,
],
'Not drupal' => [
'expected_version_string' => FALSE,
'schema_version' => "not drupal I guess",
'exception' => NULL,
'system_table_exists' => FALSE,
],
'D5 almost' => [
'expected_version_string' => FALSE,
'schema_version' => '123',
'exception' => NULL,
'system_table_exists' => TRUE,
],
'D5/6/7 Exception' => [
'expected_version_string' => FALSE,
'schema_version' => NULL,
'exception' => new DatabaseExceptionWrapper(),
'system_table_exists' => TRUE,
],
'D8/9 Exception' => [
'expected_version_string' => FALSE,
'schema_version' => NULL,
'exception' => new DatabaseExceptionWrapper(),
'system_table_exists' => FALSE,
],
];
}
}
/**
* Test class that uses the trait we are testing.
*/
class TestMigrationConfigurationTrait {
use MigrationConfigurationTrait;
}

View File

@@ -0,0 +1,618 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Unit;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\MigrationState;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use org\bovigo\vfs\vfsStreamWrapper;
/**
* Defines a class for testing \Drupal\migrate_drupal\MigrationState.
*
* @group migrate_drupal
*
* @coversDefaultClass \Drupal\migrate_drupal\MigrationState
*/
class MigrationStateUnitTest extends UnitTestCase {
/**
* Tests ::getUpgradeStates.
*
* @dataProvider providerGetUpgradeStates
*
* @covers ::getUpgradeStates
* @covers ::buildDiscoveredDestinationsBySource
* @covers ::buildDeclaredStateBySource
* @covers ::buildUpgradeState
* @covers ::getMigrationStates
* @covers ::getSourceState
* @covers ::getDestinationsForSource
*/
public function testGetUpgradeStates($modules_to_enable, $files, $field_plugins, $migrations, $source_system_data, $expected_7, $expected_6): void {
$fieldPluginManager = $this->prophesize(MigrateFieldPluginManagerInterface::class);
$fieldPluginManager->getDefinitions()->willReturn($field_plugins);
$moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
$moduleHandler->getModuleList()->willReturn($modules_to_enable);
vfsStreamWrapper::register();
$root = new vfsStreamDirectory('modules');
vfsStreamWrapper::setRoot($root);
$url = vfsStream::url('modules');
foreach ($files as $module => $contents) {
$path = $url . '/' . $module . '/migrations/state';
mkdir($path, 0755, TRUE);
file_put_contents($path . '/' . $module . '.migrate_drupal.yml', $contents);
}
$moduleHandler->getModuleDirectories()
->willReturn(array_combine(array_keys($files), array_map(function ($module) use ($url) {
return $url . '/' . $module;
}, array_keys($files))));
$migrationState = new MigrationState($fieldPluginManager->reveal(), $moduleHandler->reveal(), $this->createMock(MessengerInterface::class), $this->getStringTranslationStub());
$all_migrations = [];
foreach ($migrations as $name => $values) {
$migration = $this->prophesize(MigrationInterface::class);
$source = $this->prophesize(MigrateSourceInterface::class);
$destination = $this->prophesize(MigrateDestinationInterface::class);
$source->getSourceModule()->willReturn($values['source_module']);
$destination->getDestinationModule()
->willReturn($values['destination_module']);
$migration->getSourcePlugin()->willReturn($source->reveal());
$migration->getDestinationPlugin()->willReturn($destination->reveal());
$migration->getPluginId()->willReturn($name);
$migration->label()->willReturn($name);
$all_migrations[] = $migration->reveal();
}
// Tests Drupal 7 states.
$states = $migrationState->getUpgradeStates(7, $source_system_data, $all_migrations);
$this->assertEquals($expected_7, $states);
$source_system_data['module']['content'] = [
'name' => 'content',
'status' => TRUE,
];
// Tests Drupal 6 states.
unset($source_system_data['module']['rdf'], $source_system_data['module']['filter']);
$states = $migrationState->getUpgradeStates(6, $source_system_data, []);
$this->assertEquals($expected_6, $states);
}
/**
* Data provider for testGetUpgradeStates.
*/
public static function providerGetUpgradeStates() {
// Tests multiple scenarios:
// Not enabled and not declared.
// Destination module is not enabled.
// Destination module not enabled.
// Declared not finished.
// Not finished.
// No discovered or declared state.
// Declared finished by one module but not finished by another.
// Not declared and non compatible field plugin.
// Update path not needed.
$tests[0] = [
'modules_to_enable' => [
'entity_test' => [],
'node' => [],
'link' => [],
'rdf' => [],
],
'files' => [
'node' => <<<NODE
finished:
6:
content: node
node: node
7:
node: node
NODE
,
'entity_test' => <<<ENTITY_TEST
not_finished:
6:
entity_test: entity_test
7:
entity_test:
- entity_test
- entity_test_rev
ENTITY_TEST
,
'comment' => <<<COMMENT
finished:
6:
comment:
- comment
- node
7:
comment:
- comment
- node
COMMENT
,
'user' => <<<USER
finished:
6:
user: user
7:
user: user
USER
,
'profile' => <<<PROFILE
not_finished:
6:
profile: user
7:
profile: user
PROFILE
,
],
'field_plugins' => [
'datetime' => [
'id' => 'datetime',
'core' => [7],
'source_module' => 'date',
'destination_module' => 'datetime',
],
'link' => [
'id' => 'link',
'core' => [6, 7],
'source_module' => 'link',
'destination_module' => 'link',
],
],
'migrations' => [
'rdf' => [
'source_module' => 'rdf',
'destination_module' => 'rdf',
],
'filter' => [
'source_module' => 'filter',
'destination_module' => 'filter',
],
],
'source_system_data' => [
'module' => [
'entity_test' => [
'name' => 'entity_test',
'status' => TRUE,
],
'rdf' => [
'name' => 'rdf',
'status' => TRUE,
],
'node' => [
'name' => 'node',
'status' => TRUE,
],
'date' => [
'name' => 'date',
'status' => TRUE,
],
'link' => [
'name' => 'link',
'status' => TRUE,
],
'search' => [
'name' => 'search',
'status' => TRUE,
],
'filter' => [
'name' => 'filter',
'status' => TRUE,
],
'comment' => [
'name' => 'comment',
'status' => TRUE,
],
'standard' => [
'name' => 'standard',
'status' => TRUE,
],
'color' => [
'name' => 'color',
'status' => TRUE,
],
'user' => [
'name' => 'user',
'status' => TRUE,
],
'profile' => [
'name' => 'profile',
'status' => TRUE,
],
// Disabled, hence ignored.
'dblog' => [
'name' => 'dblog',
'status' => FALSE,
],
],
],
'expected_7' => [
MigrationState::NOT_FINISHED => [
// Not enabled and not declared.
'color' => '',
// Destination module comment is not enabled.
'comment' => 'comment, node',
// Destination module not enabled.
'date' => 'datetime',
// Declared not finished.
'entity_test' => 'entity_test, entity_test_rev',
// Destination module not enabled.
'filter' => 'filter',
// Not finished.
'profile' => 'user',
// No discovered or declared state.
'search' => '',
// Declared finished by one module but not finished by another.
'user' => 'user',
// Enabled and not declared.
'rdf' => 'rdf',
'link' => 'link',
],
MigrationState::FINISHED => [
'node' => 'node',
],
],
'expected_6' => [
MigrationState::NOT_FINISHED => [
// Declared not finished.
'entity_test' => 'entity_test',
// Destination module comment is not enabled.
'comment' => 'comment, node',
'user' => 'user',
// Not finished.
'profile' => 'user',
// Not declared and non compatible field plugin.
'date' => '',
// No discovered or declared state.
'search' => '',
'color' => '',
'link' => 'link',
],
MigrationState::FINISHED => [
'node' => 'node',
'content' => 'node',
],
],
];
// Test menu migration with all three required destination modules enabled.
$tests[1] = [
'modules_to_enable' => [
'menu_link_content' => [],
'menu_ui' => [],
'system' => [],
],
'files' => [
'system' => <<<SYSTEM
finished:
6:
menu:
- system
- menu_link_content
- menu_ui
7:
menu:
- system
- menu_link_content
- menu_ui
SYSTEM
,
'menu_link_content' => <<<MENU_LINK_CONTENT
finished:
6:
menu: menu_link_content
7:
menu: menu_link_content
MENU_LINK_CONTENT
,
'menu' => <<<MENU_UI
finished:
6:
menu: menu_ui
7:
menu: menu_ui
MENU_UI
,
],
'field_plugins' => [],
'migrations' => [
'system' => [
'source_module' => 'menu',
'destination_module' => 'system',
],
'menu_ui' => [
'source_module' => 'menu',
'destination_module' => 'menu_ui',
],
'menu_link_content' => [
'source_module' => 'menu',
'destination_module' => 'menu_link_content',
],
],
'source_system_data' => [
'module' => [
'menu' => [
'name' => 'menu',
'status' => TRUE,
],
'system' => [
'name' => 'system',
'status' => TRUE,
],
],
],
'expected_7' => [
MigrationState::NOT_FINISHED => [
'system' => '',
],
MigrationState::FINISHED => [
'menu' => 'menu_link_content, menu_ui, system',
],
],
'expected_6' => [
MigrationState::NOT_FINISHED => [
'system' => '',
'content' => '',
],
MigrationState::FINISHED => [
'menu' => 'menu_link_content, menu_ui, system',
],
],
];
// Test menu migration with menu_link_content uninstalled.
$tests[2] = $tests[1];
unset($tests[2]['modules_to_enable']['menu_link_content']);
unset($tests[2]['files']['menu_link_content']);
unset($tests[2]['migrations']['menu_link_content']);
$tests[2]['expected_7'] = [
MigrationState::NOT_FINISHED => [
'menu' => 'menu_link_content, menu_ui, system',
'system' => '',
],
];
$tests[2]['expected_6'] = [
MigrationState::NOT_FINISHED => [
'menu' => 'menu_link_content, menu_ui, system',
'system' => '',
'content' => '',
],
];
// Test menu migration with menu_ui uninstalled.
$tests[3] = $tests[1];
unset($tests[3]['modules_to_enable']['menu_ui']);
unset($tests[3]['migrations']['menu_ui']);
$tests[3]['expected_7'] = [
MigrationState::NOT_FINISHED => [
'menu' => 'menu_link_content, menu_ui, system',
'system' => '',
],
];
$tests[3]['expected_6'] = [
MigrationState::NOT_FINISHED => [
'menu' => 'menu_link_content, menu_ui, system',
'system' => '',
'content' => '',
],
];
// Test an i18n migration with all three required destination modules
// enabled.
$tests[4] = [
'modules_to_enable' => [
'block' => [],
'block_content' => [],
'content_translation' => [],
'system' => [],
],
'files' => [
'system' => <<<SYSTEM
finished:
6:
i18nblocks:
- block
- block_content
- content_translation
7:
i18nblocks:
- block
- block_content
- content_translation
SYSTEM
,
'block' => <<<BLOCK
finished:
6:
block: block
7:
block: block
BLOCK
,
'block_content' => <<<BLOCK_CONTENT
finished:
6:
block: block_content
7:
block: block_content
BLOCK_CONTENT
,
],
'field_plugins' => [],
'migrations' => [
'block' => [
'source_module' => 'block',
'destination_module' => 'block',
],
'block_content' => [
'source_module' => 'block',
'destination_module' => 'block_content',
],
'i18nblocks' => [
'source_module' => 'i18nblocks',
'destination_module' => 'content_translation',
],
],
'source_system_data' => [
'module' => [
'block' => [
'name' => 'block',
'status' => TRUE,
],
'i18nblocks' => [
'name' => 'i18nblocks',
'status' => TRUE,
],
'system' => [
'name' => 'system',
'status' => TRUE,
],
],
],
'expected_7' => [
MigrationState::NOT_FINISHED => [
'system' => '',
],
MigrationState::FINISHED => [
'block' => 'block, block_content',
'i18nblocks' => 'block, block_content, content_translation',
],
],
'expected_6' => [
MigrationState::NOT_FINISHED => [
'system' => '',
'content' => '',
],
MigrationState::FINISHED => [
'block' => 'block, block_content',
'i18nblocks' => 'block, block_content, content_translation',
],
],
];
// Test i18n_block migration with block uninstalled.
$tests[5] = $tests[4];
unset($tests[5]['modules_to_enable']['block']);
unset($tests[5]['files']['block']);
unset($tests[5]['migrations']['block']);
$tests[5]['expected_7'] = [
MigrationState::NOT_FINISHED => [
'system' => '',
'i18nblocks' => 'block, block_content, content_translation',
],
MigrationState::FINISHED => [
'block' => 'block_content',
],
];
$tests[5]['expected_6'] = [
MigrationState::NOT_FINISHED => [
'system' => '',
'content' => '',
'i18nblocks' => 'block, block_content, content_translation',
],
MigrationState::FINISHED => [
'block' => 'block_content',
],
];
// Tests modules that don't require an upgrade path.
$tests[6] = [
'modules_to_enable' => [
'system' => [],
'content_translation' => [],
],
'files' => [
'system' => <<<SYSTEM
finished:
6:
help: core
i18ncontent: content_translation
7:
help: core
i18ncontent: content_translation
SYSTEM
,
],
'field_plugins' => [],
'migrations' => [],
'source_system_data' => [
'module' => [
'system' => [
'name' => 'system',
'status' => TRUE,
],
'help' => [
'name' => 'help',
'status' => TRUE,
],
'i18ncontent' => [
'name' => 'i18ncontent',
'status' => TRUE,
],
],
],
'expected_7' => [
MigrationState::NOT_FINISHED => [
'system' => '',
],
MigrationState::FINISHED => [
'help' => 'core',
'i18ncontent' => 'content_translation',
],
],
'expected_6' => [
MigrationState::NOT_FINISHED => [
'system' => '',
'content' => '',
],
MigrationState::FINISHED => [
'help' => 'core',
'i18ncontent' => 'content_translation',
],
],
];
$tests[7] = $tests[6];
unset($tests[7]['modules_to_enable']['content_translation']);
$tests[7]['expected_7'] = [
MigrationState::NOT_FINISHED => [
'system' => '',
'i18ncontent' => 'content_translation',
],
MigrationState::FINISHED => [
'help' => 'core',
],
];
$tests[7]['expected_6'] = [
MigrationState::NOT_FINISHED => [
'system' => '',
'content' => '',
'i18ncontent' => 'content_translation',
],
MigrationState::FINISHED => [
'help' => 'core',
],
];
return $tests;
}
}

View File

@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Unit\source;
use Drupal\Core\Database\Connection;
use Drupal\Tests\migrate\Unit\MigrateTestCase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* @coversDefaultClass \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase
* @group migrate_drupal
*/
class DrupalSqlBaseTest extends MigrateTestCase {
/**
* Define bare minimum migration configuration.
*/
protected $migrationConfiguration = [
'id' => 'DrupalSqlBase',
];
/**
* @var \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase
*/
protected $base;
/**
* The plugin definition.
*
* @var array
*/
protected $pluginDefinition = [];
/**
* Mock StateInterface.
*
* @var \PHPUnit\Framework\MockObject\MockObject
*/
protected $state;
/**
* Mock entity type manager.
*
* @var \PHPUnit\Framework\MockObject\MockObject
*/
protected $entityTypeManager;
/**
* Minimum database contents needed to test DrupalSqlBase.
*/
protected $databaseContents = [
'system' => [
[
'filename' => 'sites/all/modules/module1',
'name' => 'module1',
'type' => 'module',
'status' => 0,
'schema_version' => -1,
],
],
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->pluginDefinition['requirements_met'] = TRUE;
$this->pluginDefinition['source_module'] = 'module1';
$this->state = $this->createMock('Drupal\Core\State\StateInterface');
$this->entityTypeManager = $this->createMock('Drupal\Core\Entity\EntityTypeManagerInterface');
}
/**
* @covers ::checkRequirements
*/
public function testSourceProviderNotActive(): void {
$plugin = new TestDrupalSqlBase([], 'placeholder_id', $this->pluginDefinition, $this->getMigration(), $this->state, $this->entityTypeManager);
$plugin->setDatabase($this->getDatabase($this->databaseContents));
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('The module module1 is not enabled in the source site.');
try {
$plugin->checkRequirements();
}
catch (RequirementsException $e) {
// Ensure requirements are set on the exception.
$this->assertEquals(['source_module' => 'module1'], $e->getRequirements());
// Re-throw so PHPUnit can assert the exception.
throw $e;
}
}
/**
* @covers ::checkRequirements
*/
public function testSourceDatabaseError(): void {
$plugin = new TestDrupalSqlBase([], 'test', $this->pluginDefinition, $this->getMigration(), $this->state, $this->entityTypeManager);
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('No database connection configured for source plugin test');
$plugin->checkRequirements();
}
/**
* @covers ::checkRequirements
*
* @param bool $success
* True if this test will not throw an exception.
* @param null|string $minimum_version
* The minimum version declared in the configuration of a source plugin.
* @param string $schema_version
* The schema version for the source module declared in a source plugin.
*
* @dataProvider providerMinimumVersion
*/
public function testMinimumVersion($success, $minimum_version, $schema_version): void {
$this->pluginDefinition['minimum_version'] = $minimum_version;
$this->databaseContents['system'][0]['status'] = 1;
$this->databaseContents['system'][0]['schema_version'] = $schema_version;
$plugin = new TestDrupalSqlBase([], 'test', $this->pluginDefinition, $this->getMigration(), $this->state, $this->entityTypeManager);
$plugin->setDatabase($this->getDatabase($this->databaseContents));
if (!$success) {
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage("Required minimum version $minimum_version");
}
$plugin->checkRequirements();
}
/**
* Provides data for testMinimumVersion.
*/
public static function providerMinimumVersion() {
return [
'minimum less than schema' => [
TRUE,
'7000',
'7001',
],
'same version' => [
TRUE,
'7001',
'7001',
],
'minimum greater than schema' => [
FALSE,
'7005',
'7001',
],
'schema version 0' => [
FALSE,
'7000',
'0',
],
'schema version -1' => [
FALSE,
'7000',
'-1',
],
'minimum not set' => [
TRUE,
NULL,
'-1',
],
];
}
}
/**
* Extends the DrupalSqlBase abstract class.
*/
class TestDrupalSqlBase extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function fields() {
return [];
}
/**
* {@inheritdoc}
*/
public function query() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* Tweaks DrupalSqlBase to set a new database connection for tests.
*
* @param \Drupal\Core\Database\Connection $database
* The new connection to use.
*
* @see \Drupal\Tests\migrate\Unit\MigrateSourceSqlTestCase
*/
public function setDatabase(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [];
}
}

View File

@@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal\Unit\source\d6;
use Drupal\Tests\migrate\Unit\MigrateTestCase;
// cspell:ignore throttleable
/**
* Tests the D6 SQL base class.
*
* @group migrate_drupal
*/
class Drupal6SqlBaseTest extends MigrateTestCase {
/**
* Define bare minimum migration configuration.
*/
protected $migrationConfiguration = [
'id' => 'Drupal6SqlBase',
];
/**
* @var \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase
*/
protected $base;
/**
* Minimum database contents needed to test Drupal6SqlBase.
*/
protected $databaseContents = [
'system' => [
[
'filename' => 'sites/all/modules/module1',
'name' => 'module1',
'type' => 'module',
'status' => 1,
'schema_version' => -1,
],
[
'filename' => 'sites/all/modules/module2',
'name' => 'module2',
'type' => 'module',
'status' => 0,
'schema_version' => 7201,
],
[
'filename' => 'sites/all/modules/test2',
'name' => 'test2',
'type' => 'theme',
'status' => 1,
'schema_version' => -1,
],
],
'variable' => [
[
'name' => 'my_variable',
'value' => 'b:1;',
],
],
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$plugin = 'placeholder_id';
/** @var \Drupal\Core\State\StateInterface $state */
$state = $this->createMock('Drupal\Core\State\StateInterface');
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $this->createMock('Drupal\Core\Entity\EntityTypeManagerInterface');
$this->base = new TestDrupal6SqlBase($this->migrationConfiguration, $plugin, [], $this->getMigration(), $state, $entity_type_manager);
$this->base->setDatabase($this->getDatabase($this->databaseContents));
}
/**
* Tests for Drupal6SqlBase::getSystemData().
*/
public function testGetSystemData(): void {
$system_data = $this->base->getSystemData();
// Should be 1 theme and 2 modules.
$this->assertCount(1, $system_data['theme']);
$this->assertCount(2, $system_data['module']);
// Calling again should be identical.
$this->assertSame($system_data, $this->base->getSystemData());
}
/**
* Tests for Drupal6SqlBase::moduleExists().
*/
public function testDrupal6ModuleExists(): void {
// This module should exist.
$this->assertTrue($this->base->moduleExistsWrapper('module1'));
// These modules should not exist.
$this->assertFalse($this->base->moduleExistsWrapper('module2'));
$this->assertFalse($this->base->moduleExistsWrapper('module3'));
}
/**
* Tests for Drupal6SqlBase::getModuleSchemaVersion().
*/
public function testGetModuleSchemaVersion(): void {
// Non-existent module.
$this->assertFalse($this->base->getModuleSchemaVersionWrapper('module3'));
// Disabled module should still return schema version.
$this->assertEquals(7201, $this->base->getModuleSchemaVersionWrapper('module2'));
// Enabled module.
$this->assertEquals(-1, $this->base->getModuleSchemaVersionWrapper('module1'));
}
/**
* Tests for Drupal6SqlBase::variableGet().
*/
public function testVariableGet(): void {
// Test default value.
$this->assertEquals('my_default', $this->base->variableGetWrapper('non_existent_variable', 'my_default'));
// Test non-default.
$this->assertTrue($this->base->variableGetWrapper('my_variable', FALSE));
}
}
namespace Drupal\Tests\migrate_drupal\Unit\source\d6;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Extends the Drupal6SqlBase abstract class.
*/
class TestDrupal6SqlBase extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function fields() {
return [
'filename' => $this->t('The path of the primary file for this item.'),
'name' => $this->t('The name of the item; e.g. node.'),
'type' => $this->t('The type of the item, either module, theme, or theme_engine.'),
'owner' => $this->t("A theme's 'parent'. Can be either a theme or an engine."),
'status' => $this->t('Boolean indicating whether or not this item is enabled.'),
'throttle' => $this->t('Boolean indicating whether this item is disabled when the throttle.module disables throttleable items.'),
'bootstrap' => $this->t("Boolean indicating whether this module is loaded during Drupal's early bootstrapping phase (e.g. even before the page cache is consulted)."),
'schema_version' => $this->t("The module's database schema version number."),
'weight' => $this->t("The order in which this module's hooks should be invoked."),
'info' => $this->t("A serialized array containing information from the module's .info file."),
];
}
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->database
->select('system', 's')
->fields('s', ['filename', 'name', 'schema_version']);
return $query;
}
/**
* Tweaks Drupal6SqlBase to set a new database connection for tests.
*
* @param \Drupal\Core\Database\Connection $database
* The new connection to use.
*
* @see \Drupal\Tests\migrate\Unit\MigrateSqlTestCase
*/
public function setDatabase(Connection $database) {
$this->database = $database;
}
/**
* Tweaks Drupal6SqlBase to set a new module handler for tests.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The new module handler to use.
*
* @see \Drupal\Tests\migrate\Unit\MigrateSqlTestCase
*/
public function setModuleHandler(ModuleHandlerInterface $module_handler) {
$this->moduleHandler = $module_handler;
}
/**
* Wrapper method to test protected method moduleExists().
*/
public function moduleExistsWrapper($module) {
return parent::moduleExists($module);
}
/**
* Wrapper method to test protected method getModuleSchemaVersion().
*/
public function getModuleSchemaVersionWrapper($module) {
return parent::getModuleSchemaVersion($module);
}
/**
* Wrapper method to test protected method variableGet().
*/
public function variableGetWrapper($name, $default) {
return parent::variableGet($name, $default);
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [];
}
}