clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Controller;
use Drupal\Component\Utility\Html;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\PagerSelectExtender;
use Drupal\Core\Database\Query\TableSortExtender;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_plus\Entity\MigrationGroupInterface;
use Drupal\migrate_plus\Entity\MigrationInterface as MigratePlusMigrationInterface;
use Drupal\migrate_tools\MigrateTools;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for migrate_tools message routes.
*/
class MessageController extends ControllerBase {
protected Connection $database;
protected MigrationPluginManagerInterface $migrationPluginManager;
/**
* Constructs a MessageController object.
*
* @param \Drupal\Core\Database\Connection $database
* A database connection.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
*/
public function __construct(Connection $database, MigrationPluginManagerInterface $migration_plugin_manager) {
$this->database = $database;
$this->migrationPluginManager = $migration_plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new static(
$container->get('database'),
$container->get('plugin.manager.migration')
);
}
/**
* Gets an array of log level classes.
*
* @return array
* An array of log level classes.
*/
public static function getLogLevelClassMap(): array {
return [
MigrationInterface::MESSAGE_INFORMATIONAL => 'migrate-message-4',
MigrationInterface::MESSAGE_NOTICE => 'migrate-message-3',
MigrationInterface::MESSAGE_WARNING => 'migrate-message-2',
MigrationInterface::MESSAGE_ERROR => 'migrate-message-1',
];
}
/**
* Displays a listing of migration messages.
*
* Messages are truncated at 56 chars.
*
* @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group
* The migration group.
* @param \Drupal\migrate_plus\Entity\MigrationInterface $migration
* The $migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function overview(MigrationGroupInterface $migration_group, MigratePlusMigrationInterface $migration): array {
$header = [];
$build = [];
$rows = [];
$classes = static::getLogLevelClassMap();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration_plugin */
$migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray());
$source_id_field_names = array_keys($migration_plugin->getSourcePlugin()->getIds());
$column_number = 1;
foreach ($source_id_field_names as $source_id_field_name) {
$header[] = [
'data' => $source_id_field_name,
'field' => 'sourceid' . $column_number++,
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
];
}
$header[] = [
'data' => $this->t('Severity level'),
'field' => 'level',
'class' => [RESPONSIVE_PRIORITY_LOW],
];
$header[] = [
'data' => $this->t('Message'),
'field' => 'message',
];
$header[] = [
'data' => $this->t('Destination ID'),
'field' => 'destid',
];
$header[] = [
'data' => $this->t('Status'),
'field' => 'source_row_status',
];
$result = [];
$message_table = $migration_plugin->getIdMap()->messageTableName();
if ($this->database->schema()->tableExists($message_table)) {
$map_table = $migration_plugin->getIdMap()->mapTableName();
$query = $this->database->select($message_table, 'msg')
->extend(PagerSelectExtender::class)
->extend(TableSortExtender::class);
$query->innerJoin($map_table, 'map', 'msg.source_ids_hash=map.source_ids_hash');
$query->fields('msg');
$query->fields('map');
$result = $query
->limit(50)
->orderByHeader($header)
->execute();
}
$level_mapping = MigrateTools::getLogLevelLabelMapping();
$status_mapping = MigrateTools::getStatusLevelLabelMapping();
foreach ($result as $message_row) {
$column_number = 1;
$data = [];
foreach ($source_id_field_names as $source_id_field_name) {
$column_name = 'sourceid' . $column_number++;
$data[$column_name] = $message_row->$column_name;
}
$data['level'] = $level_mapping[$message_row->level] ?: $message_row->level;
$data['message'] = $message_row->message;
$column_number = 1;
foreach ($migration_plugin->getDestinationPlugin()->getIds() as $dest_id_field_name => $dest_id_schema) {
$column_name = 'destid' . $column_number++;
$data['destid']['data'][] = $message_row->$column_name;
$data['destid']['#destination_fields'][$dest_id_field_name] =
$data['destid']['#destination_fields'][$column_name] = $message_row->$column_name;
}
$destid = array_filter($data['destid']['data']);
$data['destid']['data'] = [
'#markup' => $destid ? implode(MigrateTools::DEFAULT_ID_LIST_DELIMITER, $data['destid']['data']) : '',
];
$data['status'] = $status_mapping[$message_row->source_row_status];
$rows[] = [
'class' => [
Html::getClass('migrate-message-' . $message_row->level),
$classes[$message_row->level],
],
'data' => $data,
];
}
$build['message_table'] = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#attributes' => ['id' => $message_table, 'class' => [$message_table]],
'#empty' => $this->t('No messages for this migration.'),
];
$build['message_pager'] = ['#type' => 'pager'];
return $build;
}
/**
* Get the title of the page.
*
* @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group
* The migration group.
* @param \Drupal\migrate_plus\Entity\MigrationInterface $migration
* The $migration.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The translated title.
*/
public function title(MigrationGroupInterface $migration_group, MigratePlusMigrationInterface $migration): TranslatableMarkup {
return $this->t(
'Messages of %migration',
['%migration' => $migration->label()]
);
}
}

View File

@@ -0,0 +1,272 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Controller;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\Core\Url;
use Drupal\migrate\MigrateMessage;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_plus\Entity\MigrationGroupInterface;
use Drupal\migrate_plus\Entity\MigrationInterface;
use Drupal\migrate_tools\MigrateBatchExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Returns responses for migrate_tools migration view routes.
*/
class MigrationController extends ControllerBase implements ContainerInjectionInterface {
protected MigrationPluginManagerInterface $migrationPluginManager;
protected CurrentRouteMatch $currentRouteMatch;
/**
* Constructs a new MigrationController object.
*
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The plugin manager for config entity-based migrations.
* @param \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatch
* The current route match.
*/
public function __construct(MigrationPluginManagerInterface $migration_plugin_manager, CurrentRouteMatch $currentRouteMatch) {
$this->migrationPluginManager = $migration_plugin_manager;
$this->currentRouteMatch = $currentRouteMatch;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new static(
$container->get('plugin.manager.migration'),
$container->get('current_route_match')
);
}
/**
* Displays an overview of a migration entity.
*
* @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group
* The migration group.
* @param \Drupal\migrate_plus\Entity\MigrationInterface $migration
* The $migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function overview(MigrationGroupInterface $migration_group, MigrationInterface $migration): array {
$build = [];
$build['overview'] = [
'#type' => 'fieldset',
'#title' => $this->t('Overview'),
];
$build['overview']['group'] = [
'#title' => $this->t('Group:'),
'#markup' => Xss::filterAdmin($migration_group->label()),
'#type' => 'item',
];
$build['overview']['description'] = [
'#title' => $this->t('Description:'),
'#markup' => Xss::filterAdmin($migration->label()),
'#type' => 'item',
];
$migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray());
$migration_dependencies = $migration_plugin->getMigrationDependencies();
if (!empty($migration_dependencies['required'])) {
$build['overview']['dependencies'] = [
'#title' => $this->t('Migration Dependencies') ,
'#markup' => Xss::filterAdmin(implode(', ', $migration_dependencies['required'])),
'#type' => 'item',
];
}
if (!empty($migration_dependencies['optional'])) {
$build['overview']['soft_dependencies'] = [
'#title' => $this->t('Soft Migration Dependencies'),
'#markup' => Xss::filterAdmin(implode(', ', $migration_dependencies['optional'])),
'#type' => 'item',
];
}
return $build;
}
/**
* Display source information of a migration entity.
*
* @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group
* The migration group.
* @param \Drupal\migrate_plus\Entity\MigrationInterface $migration
* The $migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function source(MigrationGroupInterface $migration_group, MigrationInterface $migration): array {
$build = [];
// Source field information.
$build['source'] = [
'#type' => 'fieldset',
'#title' => $this->t('Source'),
'#group' => 'detail',
'#description' => $this->t('<p>These are the fields available from the source of this migration task. The machine names listed here may be used as sources in the process pipeline.</p>'),
'#description_display' => 'after',
'#attributes' => [
'id' => 'migration-detail-source',
],
];
$migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray());
$source = $migration_plugin->getSourcePlugin();
$build['source']['query'] = [
'#type' => 'item',
'#title' => $this->t('Query'),
'#markup' => '<pre>' . Xss::filterAdmin($source) . '</pre>',
];
$header = [$this->t('Machine name'), $this->t('Description')];
$rows = [];
foreach ($source->fields($migration_plugin) as $machine_name => $description) {
$rows[] = [
['data' => Html::escape($machine_name)],
['data' => Xss::filterAdmin($description)],
];
}
$build['source']['fields'] = [
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No fields'),
];
return $build;
}
/**
* Display process information of a migration entity.
*
* @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group
* The migration group.
* @param \Drupal\migrate_plus\Entity\MigrationInterface $migration
* The $migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function process(MigrationGroupInterface $migration_group, MigrationInterface $migration): array {
$build = [];
$migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray());
// Process information.
$build['process'] = [
'#type' => 'fieldset',
'#title' => $this->t('Process'),
];
$header = [
$this->t('Destination'),
$this->t('Source'),
$this->t('Process plugin'),
$this->t('Default'),
];
$rows = [];
foreach ($migration_plugin->getProcess() as $destination_id => $process_line) {
$row = [];
$row[] = ['data' => Html::escape($destination_id)];
if (isset($process_line[0]['source'])) {
if (is_array($process_line[0]['source'])) {
$process_line[0]['source'] = implode(', ', $process_line[0]['source']);
}
$row[] = ['data' => Xss::filterAdmin($process_line[0]['source'])];
}
else {
$row[] = '';
}
if (isset($process_line[0]['plugin'])) {
$process_line_plugins = [];
foreach ($process_line as $process_line_row) {
$process_line_plugins[] = Xss::filterAdmin($process_line_row['plugin']);
}
$row[] = ['data' => implode(', ', $process_line_plugins)];
}
else {
$row[] = '';
}
if (isset($process_line[0]['default_value'])) {
$row[] = ['data' => Xss::filterAdmin($process_line[0]['default_value'])];
}
else {
$row[] = '';
}
$rows[] = $row;
}
$build['process']['fields'] = [
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No process defined.'),
];
return $build;
}
/**
* Displays destination information of a migration entity.
*
* @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group
* The migration group.
* @param \Drupal\migrate_plus\Entity\MigrationInterface $migration
* The $migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function destination(MigrationGroupInterface $migration_group, MigrationInterface $migration): array {
$build = [];
$migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray());
// Destination field information.
$build['destination'] = [
'#type' => 'fieldset',
'#title' => $this->t('Destination'),
'#group' => 'detail',
'#description' => $this->t('<p>These are the fields available in the destination plugin of this migration task. The machine names are those available to be used as the keys in the process pipeline.</p>'),
'#description_display' => 'after',
'#attributes' => [
'id' => 'migration-detail-destination',
],
];
$destination = $migration_plugin->getDestinationPlugin();
$build['destination']['type'] = [
'#type' => 'item',
'#title' => $this->t('Type'),
'#markup' => Xss::filterAdmin($destination->getPluginId()),
];
$header = [$this->t('Machine name'), $this->t('Description')];
$rows = [];
$destination_fields = $destination->fields() ?: [];
foreach ($destination_fields as $machine_name => $description) {
$rows[] = [
['data' => Html::escape($machine_name)],
['data' => Xss::filterAdmin($description)],
];
}
$build['destination']['fields'] = [
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No fields'),
];
return $build;
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Controller;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
/**
* Provides a listing of migration group entities.
*
* @package Drupal\migrate_tools\Controller
*
* @ingroup migrate_tools
*/
class MigrationGroupListBuilder extends ConfigEntityListBuilder {
/**
* Builds the header row for the entity listing.
*
* @return array
* A render array structure of header strings.
*
* @see \Drupal\Core\Entity\Controller\EntityListController::render()
*/
public function buildHeader(): array {
$header = [];
$header['label'] = $this->t('Migration Group');
$header['machine_name'] = $this->t('Machine Name');
$header['description'] = $this->t('Description');
$header['source_type'] = $this->t('Source Type');
return $header + parent::buildHeader();
}
/**
* Builds a row for an entity in the entity listing.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which to build the row.
*
* @return array
* A render array of the table row for displaying the entity.
*
* @see \Drupal\Core\Entity\EntityListController::render()
*/
public function buildRow(EntityInterface $entity): array {
$row = [];
$row['label'] = $entity->label();
$row['machine_name'] = $entity->id();
$row['description'] = $entity->get('description');
$row['source_type'] = $entity->get('source_type');
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity): array {
$operations = parent::getDefaultOperations($entity);
$operations['list'] = [
'title' => $this->t('List migrations'),
'weight' => 0,
'url' => Url::fromRoute('entity.migration.list', ['migration_group' => $entity->id()]),
];
return $operations;
}
}

View File

@@ -0,0 +1,247 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Controller;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\Core\Url;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_plus\Entity\Migration;
use Drupal\migrate_plus\Entity\MigrationGroup;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a listing of migration entities in a given group.
*
* @package Drupal\migrate_tools\Controller
*
* @ingroup migrate_tools
*/
class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHandlerInterface {
protected CurrentRouteMatch $currentRouteMatch;
protected MigrationPluginManagerInterface $migrationPluginManager;
protected LoggerInterface $logger;
/**
* Constructs a new EntityListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Routing\CurrentRouteMatch $current_route_match
* The current route match service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The plugin manager for config entity-based migrations.
* @param \Psr\Log\LoggerInterface $logger
* The logger service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, CurrentRouteMatch $current_route_match, MigrationPluginManagerInterface $migration_plugin_manager, LoggerInterface $logger) {
parent::__construct($entity_type, $storage);
$this->currentRouteMatch = $current_route_match;
$this->migrationPluginManager = $migration_plugin_manager;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type): self {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('current_route_match'),
$container->get('plugin.manager.migration'),
$container->get('logger.channel.migrate_tools')
);
}
/**
* Retrieve the migrations belonging to the appropriate group.
*
* @return array
* An array of entity IDs.
*/
protected function getEntityIds(): array {
$migration_group = $this->currentRouteMatch->getParameter('migration_group');
$query = $this->getStorage()->getQuery()
->accessCheck(TRUE)
->sort($this->entityType->getKey('id'));
$migration_groups = MigrationGroup::loadMultiple();
if (array_key_exists($migration_group, $migration_groups)) {
$query->condition('migration_group', $migration_group);
}
else {
$query->notExists('migration_group');
}
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
return $query->execute();
}
/**
* Builds the header row for the entity listing.
*
* @return array
* A render array structure of header strings.
*
* @see \Drupal\Core\Entity\EntityListController::render()
*/
public function buildHeader(): array {
$header = [];
$header['label'] = $this->t('Migration');
$header['machine_name'] = $this->t('Machine Name');
$header['status'] = $this->t('Status');
$header['total'] = $this->t('Total');
$header['imported'] = $this->t('Imported');
$header['unprocessed'] = $this->t('Unprocessed');
$header['messages'] = $this->t('Messages');
$header['last_imported'] = $this->t('Last Imported');
$header['operations'] = $this->t('Operations');
return $header;
}
/**
* Builds a row for a migration plugin.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The migration plugin for which to build the row.
*
* @return array
* A render array of the table row for displaying the plugin information.
*
* @see \Drupal\Core\Entity\EntityListController::render()
*/
public function buildRow(EntityInterface $entity): array {
$row = [];
try {
assert($entity instanceof Migration);
$migration = $this->migrationPluginManager->createInstance($entity->id());
if (!$migration) {
return $row;
}
$migration_group = $entity->get('migration_group');
if (!$migration_group) {
$migration_group = 'default';
}
$route_parameters = [
'migration_group' => $migration_group,
'migration' => $migration->id(),
];
$row['label'] = [
'data' => [
'#type' => 'link',
'#title' => $migration->label(),
'#url' => Url::fromRoute("entity.migration.overview", $route_parameters),
],
];
$row['machine_name'] = $migration->id();
$row['status'] = $migration->getStatusLabel();
}
catch (\Exception $e) {
$this->logger->warning('Migration entity id %id is malformed: %orig', [
'%id' => $entity->id(),
'%orig' => $e->getMessage(),
]);
return $row;
}
try {
// Derive the stats.
$source_plugin = $migration->getSourcePlugin();
$row['total'] = $source_plugin->count();
$map = $migration->getIdMap();
$row['imported'] = $map->importedCount();
// -1 indicates uncountable sources.
if ($row['total'] == -1) {
$row['total'] = $this->t('N/A');
$row['unprocessed'] = $this->t('N/A');
}
else {
$row['unprocessed'] = $row['total'] - $map->processedCount();
}
$row['messages'] = [
'data' => [
'#type' => 'link',
'#title' => $map->messageCount(),
'#url' => Url::fromRoute("migrate_tools.messages", $route_parameters),
],
];
$migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported');
$last_imported = $migrate_last_imported_store->get($migration->id(), FALSE);
if ($last_imported) {
/** @var \Drupal\Core\Datetime\DateFormatter $date_formatter */
$date_formatter = \Drupal::service('date.formatter');
$row['last_imported'] = $date_formatter->format((int) ($last_imported / 1000),
'custom', 'Y-m-d H:i:s');
}
else {
$row['last_imported'] = '';
}
$row['operations']['data'] = [
'#type' => 'dropbutton',
'#links' => [
'simple_form' => [
'title' => $this->t('Execute'),
'url' => Url::fromRoute('migrate_tools.execute', [
'migration_group' => $migration_group,
'migration' => $migration->id(),
]),
],
],
];
}
catch (\Throwable $throwable) {
$this->handleThrowable($row);
}
return $row;
}
/**
* Derive the row data.
*
* @param array $row
* The table row.
*/
protected function handleThrowable(array &$row): void {
$row['status'] = $this->t('No data found');
$row['total'] = $this->t('N/A');
$row['imported'] = $this->t('N/A');
$row['unprocessed'] = $this->t('N/A');
$row['messages'] = $this->t('N/A');
$row['last_imported'] = $this->t('N/A');
$row['operations'] = $this->t('N/A');
}
/**
* Add group route parameter.
*
* @param \Drupal\Core\Url $url
* The URL associated with an operation.
* @param string $migration_group
* The migration's parent group.
*/
protected function addGroupParameter(Url $url, $migration_group): void {
if (!$migration_group) {
$migration_group = 'default';
}
$route_parameters = $url->getRouteParameters() + ['migration_group' => $migration_group];
$url->setRouteParameters($route_parameters);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Drupal\migrate_tools\Discovery;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Plugin\Discovery\YamlDirectoryDiscovery;
/**
* Extends YAML directory discovery to allow BC with single-file discovery.
*
* @todo Mark plugins from the decorated discovery as deprecated.
*
* @todo Remove this in 7.0.0 and use YamlDirectoryDiscovery directly.
*/
class YamlDiscoveryDecorator extends YamlDirectoryDiscovery {
/**
* The Discovery object being decorated.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**
* Constructs a YamlDiscoveryDecorator object.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
* The discovery object that is being decorated.
* @param string $name
* The file name suffix to use for discovery; for instance, 'test' will
* become 'MODULE.test.yml'.
* @param array $directories
* An array of directories to scan.
*/
public function __construct(DiscoveryInterface $decorated, array $directories, $file_cache_key_suffix, $key = 'id') {
parent::__construct($directories, $file_cache_key_suffix, $key);
$this->decorated = $decorated;
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
return parent::getDefinitions() + $this->decorated->getDefinitions();
}
/**
* Passes through all unknown calls onto the decorated object.
*/
public function __call($method, $args) {
return call_user_func_array([$this->decorated, $method], $args);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\migrate\MigrateMessageInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
/**
* Print message in drush from migrate message. Drush 9 version.
*
* @package Drupal\migrate_tools
*/
class Drush9LogMigrateMessage implements MigrateMessageInterface, LoggerAwareInterface {
use LoggerAwareTrait;
/**
* The map between migrate status and drush log levels.
*
* @var array
*/
protected array $map = [
'status' => 'notice',
];
/**
* DrushLogMigrateMessage constructor.
*/
public function __construct(LoggerInterface $logger) {
$this->setLogger($logger);
}
/**
* Output a message from the migration.
*
* @param string $message
* The message to display.
* @param string $type
* The type of message to display.
*/
public function display($message, $type = 'status'): void {
$type = $this->map[$type] ?? $type;
$this->logger->log($type, $message);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\migrate\MigrateMessage;
use Drupal\migrate\MigrateMessageInterface;
/**
* Logger implementation for drush.
*
* @package Drupal\migrate_tools
*/
class DrushLogMigrateMessage extends MigrateMessage implements MigrateMessageInterface {
/**
* Output a message from the migration.
*
* @param string $message
* The message to display.
* @param string $type
* The type of message to display.
*
* @see drush_log()
*/
public function display($message, $type = 'status'): void {
$type = $this->map[$type] ?? RfcLogLevel::NOTICE;
\Drupal::service(('logger.channel.migrate_tools'))->log($type, $message);
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\EventSubscriber;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Plugin\MigrationInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Import and rollback progress bar.
*/
class MigrationDrushCommandProgress implements EventSubscriberInterface {
protected LoggerInterface $logger;
protected ?ProgressBar $symfonyProgressBar = NULL;
/**
* MigrationDrushCommandProgress constructor.
*
* @param \Psr\Log\LoggerInterface $logger
* The logger service.
*/
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = [];
$events[MigrateEvents::POST_ROW_SAVE][] = ['updateProgressBar', -10];
$events[MigrateEvents::MAP_DELETE][] = ['updateProgressBar', -10];
$events[MigrateEvents::POST_IMPORT][] = ['clearProgress', 10];
$events[MigrateEvents::POST_ROLLBACK][] = ['clearProgress', 10];
return $events;
}
/**
* Initializes the progress bar.
*
* This must be called before the progress bar can be used.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
* The output.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param array $options
* Additional options of the command.
*/
public function initializeProgress(OutputInterface $output, MigrationInterface $migration, array $options = []): void {
// Don't display progress bar if explicitly disabled.
if (!empty($migration->skipProgressBar)) {
return;
}
// If the source is configured to skip counts, a progress bar is not
// possible.
if (!empty($migration->getSourceConfiguration()['skip_count'])) {
return;
}
try {
// Clone so that any generators aren't initialized prematurely.
$source = clone $migration->getSourcePlugin();
$count = (int) $source->count();
// In case the --limit option is set, reduce the count.
if (array_key_exists('limit', $options) && $options['limit'] > 0 && $options['limit'] < $count) {
$count = (int) $options['limit'];
}
$this->symfonyProgressBar = new ProgressBar($output, $count);
}
catch (\Exception $exception) {
if (!empty($migration->continueOnFailure)) {
$this->logger->error($exception->getMessage());
}
else {
throw $exception;
}
}
}
/**
* Event callback for advancing the progress bar.
*/
public function updateProgressBar(): void {
if ($this->isProgressBar()) {
$this->symfonyProgressBar->advance();
}
}
/**
* Event callback for removing the progress bar after operation is finished.
*/
public function clearProgress(): void {
if ($this->isProgressBar()) {
$this->symfonyProgressBar->clear();
}
}
/**
* Determine if a progress bar should be displayed.
*
* @return bool
* TRUE if a progress bar should be displayed, FALSE otherwise.
*/
protected function isProgressBar(): bool {
// Can't do anything if the progress bar is not initialised; this probably
// means we're not running as a Drush command, therefore do nothing.
if ($this->symfonyProgressBar === NULL) {
return FALSE;
}
return TRUE;
}
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\EventSubscriber;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\Event\MigrateRowDeleteEvent;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_plus\Event\MigrateEvents as MigratePlusEvents;
use Drupal\migrate_tools\MigrateTools;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Import and sync source and destination.
*/
class MigrationImportSync implements EventSubscriberInterface {
protected EventDispatcherInterface $dispatcher;
protected MigrateTools $migrateTools;
/**
* MigrationImportSync constructor.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* The event dispatcher.
*/
public function __construct(EventDispatcherInterface $dispatcher, MigrateTools $migrateTools) {
$this->dispatcher = $dispatcher;
$this->migrateTools = $migrateTools;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = [];
$events[MigrateEvents::PRE_IMPORT][] = ['sync'];
$events[MigrateEvents::POST_IMPORT][] = ['cleanSyncData'];
return $events;
}
/**
* Event callback to sync source and destination.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The migration import event.
*
* @throws \Exception
*/
public function sync(MigrateImportEvent $event): void {
$migration = $event->getMigration();
if (!empty($migration->syncSource)) {
$migrationId = $migration->getPluginId();
// Clear Sync IDs for this migration before starting preparing rows.
$this->migrateTools->clearSyncSourceIds($migrationId);
// Activate the syncing state for this migration, so
// migrate_tools_migrate_prepare_row() can record all IDs.
$this->migrateTools->setMigrationSyncingState($migrationId, TRUE);
// Loop through the source to register existing source ids.
// @see migrate_tools_migrate_prepare_row().
// Clone so that any generators aren't initialized prematurely.
$source = clone $migration->getSourcePlugin();
$source->rewind();
while ($source->valid()) {
$source->next();
}
// Deactivate the syncing state for this migration, so
// migrate_tools_migrate_prepare_row() does not record any further IDs
// during the actual migration process.
$this->migrateTools->setMigrationSyncingState($migrationId, FALSE);
$source_id_values = $this->migrateTools->getSyncSourceIds($migrationId);
$id_map = $migration->getIdMap();
$id_map->rewind();
$destination = $migration->getDestinationPlugin();
while ($id_map->valid()) {
$map_source_id = $id_map->currentSource();
foreach ($source->getIds() as $id_key => $id_config) {
if ($id_config['type'] === 'string') {
$map_source_id[$id_key] = (string) $map_source_id[$id_key];
}
elseif ($id_config['type'] === 'integer') {
$map_source_id[$id_key] = (int) $map_source_id[$id_key];
}
}
if (!in_array($map_source_id, $source_id_values, TRUE)) {
$destination_ids = $id_map->currentDestination();
if ($destination_ids !== NULL) {
$this->dispatchRowDeleteEvent(MigrateEvents::PRE_ROW_DELETE, $migration, $destination_ids);
if (class_exists(MigratePlusEvents::class)) {
$this->dispatchRowDeleteEvent(MigratePlusEvents::MISSING_SOURCE_ITEM, $migration, $destination_ids);
}
$destination->rollback($destination_ids);
$this->dispatchRowDeleteEvent(MigrateEvents::POST_ROW_DELETE, $migration, $destination_ids);
}
$id_map->delete($map_source_id);
}
$id_map->next();
}
$this->dispatcher->dispatch(new MigrateRollbackEvent($migration), MigrateEvents::POST_ROLLBACK);
}
}
/**
* Cleans Sync data after a migration is complete.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The migration import event.
*/
public function cleanSyncData(MigrateImportEvent $event): void {
$migration = $event->getMigration();
$migrationId = $migration->getPluginId();
$this->migrateTools->clearSyncSourceIds($migrationId);
}
/**
* Dispatches MigrateRowDeleteEvent event.
*
* @param string $event_name
* The event name to dispatch.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The active migration.
* @param array $destination_ids
* The destination identifier values of the record.
*/
protected function dispatchRowDeleteEvent(string $event_name, MigrationInterface $migration, array $destination_ids): void {
// Symfony changing dispatcher so implementation could change.
$this->dispatcher->dispatch(new MigrateRowDeleteEvent($migration, $destination_ids), $event_name);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Class MigrationAddForm.
*
* Provides the add form for our migration entity.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationAddForm extends MigrationFormBase {
/**
* Returns the actions provided by this form.
*
* For our add form, we only need to change the text of the submit button.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*
* @return array
* An array of supported actions for the current entity form.
*/
protected function actions(array $form, FormStateInterface $form_state): array {
$actions = parent::actions($form, $form_state);
unset($actions['submit']);
return $actions;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
/**
* Provides the delete form for our Migration entity.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationDeleteForm extends EntityConfirmFormBase {
/**
* Gathers a confirmation question.
*
* @return string
* Translated string.
*/
public function getQuestion(): TranslatableMarkup {
return $this->t('Are you sure you want to delete migration %label?', [
'%label' => $this->entity->label(),
]);
}
/**
* Gather the confirmation text.
*
* @return string
* Translated string.
*/
public function getConfirmText(): TranslatableMarkup {
return $this->t('Delete Migration');
}
/**
* Gets the cancel URL.
*
* @return \Drupal\Core\Url
* The URL to go to if the user cancels the deletion.
*/
public function getCancelUrl(): Url {
return new Url('entity.migration_group.list');
}
/**
* The submit handler for the confirm form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// Delete the entity.
$this->entity->delete();
// Set a message that the entity was deleted.
$this->messenger()->addStatus($this->t('Migration %label was deleted.', [
'%label' => $this->entity->label(),
]));
// Redirect the user to the list controller when complete.
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides the edit form for our Migration entity.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationEditForm extends MigrationFormBase {
/**
* Returns the actions provided by this form.
*
* For the edit form, we only need to change the text of the submit button.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*
* @return array
* An array of supported actions for the current entity form.
*/
public function actions(array $form, FormStateInterface $form_state): array {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Update Migration');
return $actions;
}
/**
* Add group route parameter.
*
* @param \Drupal\Core\Url $url
* The URL associated with an operation.
* @param string $migration_group
* The migration's parent group.
*/
protected function addGroupParameter(Url $url, string $migration_group): void {
$route_parameters = $url->getRouteParameters() + ['migration_group' => $migration_group];
$url->setRouteParameters($route_parameters);
}
}

View File

@@ -0,0 +1,265 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\migrate\MigrateMessage;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_tools\MigrateBatchExecutable;
use Drupal\migrate_tools\MigrateTools;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* This form is specifically for configuring process pipelines.
*/
class MigrationExecuteForm extends FormBase {
/**
* Plugin manager for migration plugins.
*/
protected MigrationPluginManagerInterface $migrationPluginManager;
/**
* Constructs a new MigrationExecuteForm object.
*
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The plugin manager for config entity-based migrations.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(MigrationPluginManagerInterface $migration_plugin_manager, RouteMatchInterface $route_match) {
$this->migrationPluginManager = $migration_plugin_manager;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new static(
$container->get('plugin.manager.migration'),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'migration_execute_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form = $form ?: [];
/** @var \Drupal\migrate_plus\Entity\MigrationInterface $migration */
$migration = $this->getRouteMatch()->getParameter('migration');
$form['#title'] = $this->t('Execute migration %label', ['%label' => $migration->label()]);
$form = $this->buildFormOperations($form, $form_state);
$form = $this->buildFormOptions($form, $form_state);
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Execute'),
];
return $form;
}
/**
* Build the operation form field.
*
* @param array $form
* The execution form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The execution form updated with the operations.
*/
protected function buildFormOperations(array $form, FormStateInterface $form_state): array {
// Build the migration execution form.
$options = [
'import' => $this->t('Import'),
'rollback' => $this->t('Rollback'),
'stop' => $this->t('Stop'),
'reset' => $this->t('Reset'),
];
$form['operation'] = [
'#type' => 'radios',
'#title' => $this->t('Operation'),
'#description' => $this->t('Choose an operation to run.'),
'#options' => $options,
'#default_value' => 'import',
'#required' => TRUE,
'import' => [
'#description' => $this->t('Imports all previously unprocessed records from the source, plus any records marked for update, into destination Drupal objects.'),
],
'rollback' => [
'#description' => $this->t('Deletes all Drupal objects created by the import.'),
],
'stop' => [
'#description' => $this->t('Cleanly interrupts any import or rollback processes that may currently be running.'),
],
'reset' => [
'#description' => $this->t('Sometimes a process may fail to stop cleanly, and be left stuck in an Importing or Rolling Back status. Choose Reset to clear the status and permit other operations to proceed.'),
],
];
return $form;
}
/**
* Build the execution options form field.
*
* @param array $form
* The execution form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The execution form updated with the execution options.
*/
protected function buildFormOptions(array $form, FormStateInterface $form_state): array {
$form['options'] = [
'#type' => 'details',
'#title' => $this->t('Additional execution options'),
'#open' => FALSE,
];
$form['options']['update'] = [
'#type' => 'checkbox',
'#title' => $this->t('Update'),
'#description' => $this->t('Check this box to update all previously-imported content in addition to importing new content. Leave unchecked to only import new content'),
];
$form['options']['force'] = [
'#type' => 'checkbox',
'#title' => $this->t('Ignore dependencies'),
'#description' => $this->t('Check this box to ignore dependencies when running imports - all tasks will run whether or not their dependent tasks have completed.'),
];
$form['options']['limit'] = [
'#type' => 'number',
'#title' => $this->t('Limit to:'),
'#size' => 10,
'#description' => $this->t('Set a limit of how many items to process for each migration task.'),
'#min' => 1,
];
$form['options']['idlist'] = [
'#type' => 'textfield',
'#title' => $this->t('ID List'),
'#maxlength' => 255,
'#size' => 60,
'#pattern' => '^[0-9]+(' . MigrateTools::DEFAULT_ID_LIST_DELIMITER . '[0-9]+)?(,?[0-9]+(' . MigrateTools::DEFAULT_ID_LIST_DELIMITER . '[0-9]+)?)*$',
'#description' => $this->t('Comma-separated list of IDs to process.'),
'#states' => [
'enabled' => [
':input[name="operation"]' => [['value' => 'import'], 'or', ['value' => 'rollback']],
],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$migration = $this->getRouteMatch()->getParameter('migration');
if ($migration) {
$migration_id = $migration->id();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration_plugin */
$migration_plugin = $this->migrationPluginManager->createInstance($migration_id, $migration->toArray());
$migrateMessage = new MigrateMessage();
switch ($form_state->getValue('operation')) {
case 'import':
$executable = new MigrateBatchExecutable($migration_plugin, $migrateMessage, $this->buildOptions($form_state));
$executable->batchImport();
break;
case 'rollback':
$executable = new MigrateBatchExecutable($migration_plugin, $migrateMessage, $this->buildOptions($form_state));
$status = $executable->rollback();
if ($status === MigrationInterface::RESULT_COMPLETED) {
$this->messenger()->addStatus($this->t('Rollback completed', ['@id' => $migration_id]));
}
else {
$this->messenger()->addError($this->t('Rollback of !name migration failed.', ['!name' => $migration_id]));
}
break;
case 'stop':
$migration_plugin->interruptMigration(MigrationInterface::RESULT_STOPPED);
$status = $migration_plugin->getStatus();
switch ($status) {
case MigrationInterface::STATUS_IDLE:
$this->messenger()->addStatus($this->t('Migration @id is idle', ['@id' => $migration_id]));
break;
case MigrationInterface::STATUS_DISABLED:
$this->messenger()->addWarning($this->t('Migration @id is disabled', ['@id' => $migration_id]));
break;
case MigrationInterface::STATUS_STOPPING:
$this->messenger()->addWarning($this->t('Migration @id is already stopping', ['@id' => $migration_id]));
break;
default:
$migration->interruptMigration(MigrationInterface::RESULT_STOPPED);
$this->messenger()->addStatus($this->t('Migration @id requested to stop', ['@id' => $migration_id]));
break;
}
break;
case 'reset':
$status = $migration_plugin->getStatus();
if ($status === MigrationInterface::STATUS_IDLE) {
$this->messenger()->addWarning($this->t('Migration @id is already Idle', ['@id' => $migration_id]));
}
else {
$this->messenger()->addStatus($this->t('Migration @id reset to Idle', ['@id' => $migration_id]));
}
$migration_plugin->setStatus(MigrationInterface::STATUS_IDLE);
break;
}
}
}
/**
* Build migrate execute options from the submitted form values.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* Options array for the migrate execution.
*/
protected function buildOptions(FormStateInterface $form_state) {
$options = [
'limit' => $form_state->getValue('limit') ?: 0,
'update' => $form_state->getValue('update') ?: 0,
'force' => $form_state->getValue('force') ?: 0,
];
if ($idlist = $form_state->getValue('idlist')) {
$options['idlist'] = $idlist;
}
return $options;
}
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\migrate_plus\Entity\Migration;
use Drupal\migrate_plus\Entity\MigrationGroup;
/**
* Base form for a migration.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationFormBase extends EntityForm {
/**
* Overrides Drupal\Core\Entity\EntityFormController::form().
*
* Builds the entity add/edit form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*
* @return array
* An associative array containing the migration add/edit form.
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
// Get anything we need from the base class.
$form = parent::buildForm($form, $form_state);
$migration = $this->entity;
assert($migration instanceof Migration);
$form['warning'] = [
'#markup' => $this->t('Creating migrations is not yet supported. See <a href=":url">:url</a>', [
':url' => 'https://www.drupal.org/node/2573241',
]),
];
// Build the form.
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $migration->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#title' => $this->t('Machine name'),
'#default_value' => $migration->id(),
'#machine_name' => [
'exists' => [$this, 'exists'],
'replace_pattern' => '([^a-z0-9_]+)|(^custom$)',
'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".',
],
'#disabled' => !$migration->isNew(),
];
$groups = MigrationGroup::loadMultiple();
$group_options = [];
foreach ($groups as $group) {
$group_options[$group->id()] = $group->label();
}
if (!$migration->migration_group && isset($group_options['default'])) {
$migration->set('migration_group', 'default');
}
$form['migration_group'] = [
'#type' => 'select',
'#title' => $this->t('Migration Group'),
'#empty_value' => '',
'#default_value' => $migration->migration_group,
'#options' => $group_options,
'#description' => $this->t('Assign this migration to an existing group.'),
];
return $form;
}
/**
* Checks for an existing migration group.
*
* @param string|int $entity_id
* The entity ID.
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return bool
* TRUE if this format already exists, FALSE otherwise.
*/
public function exists($entity_id, array $element, FormStateInterface $form_state): bool {
$query = $this->entityTypeManager->getStorage('migration')
->getQuery()
->accessCheck(TRUE);
// Query the entity ID to see if its in use.
$result = $query->condition('id', $element['#field_prefix'] . $entity_id)
->execute();
// We don't need to return the ID, only if it exists or not.
return (bool) $result;
}
/**
* Overrides Drupal\Core\Entity\EntityFormController::actions().
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*
* @return array
* An array of supported actions for the current entity form.
*/
protected function actions(array $form, FormStateInterface $form_state): array {
// Get the basic actions from the base class.
$actions = parent::actions($form, $form_state);
// Change the submit button text.
$actions['submit']['#value'] = $this->t('Save');
// Return the result.
return $actions;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state): void {
$migration = $this->getEntity();
$status = $migration->save();
if ($status == SAVED_UPDATED) {
// If we edited an existing entity...
$this->messenger()->addStatus($this->t('Migration %label has been updated.', ['%label' => $migration->label()]));
}
else {
// If we created a new entity...
$this->messenger()->addStatus($this->t('Migration %label has been added.', ['%label' => $migration->label()]));
}
// Redirect the user back to the listing route after the save operation.
$form_state->setRedirect('entity.migration.list',
['migration_group' => $migration->get('migration_group')]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Class MigrationGroupAddForm.
*
* Provides the add form for our migration_group entity.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationGroupAddForm extends MigrationGroupFormBase {
/**
* Returns the actions provided by this form.
*
* For our add form, we only need to change the text of the submit button.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*
* @return array
* An array of supported actions for the current entity form.
*/
protected function actions(array $form, FormStateInterface $form_state): array {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Create Migration Group');
return $actions;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
/**
* Provides the delete form for our Migration Group entity.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationGroupDeleteForm extends EntityConfirmFormBase {
/**
* Gathers a confirmation question.
*
* @return string
* Translated string.
*/
public function getQuestion(): TranslatableMarkup {
return $this->t('Are you sure you want to delete migration group %label?', [
'%label' => $this->entity->label(),
]);
}
/**
* Gather the confirmation text.
*
* @return string
* Translated string.
*/
public function getConfirmText(): TranslatableMarkup {
return $this->t('Delete Migration Group');
}
/**
* Gets the cancel URL.
*
* @return \Drupal\Core\Url
* The URL to go to if the user cancels the deletion.
*/
public function getCancelUrl(): Url {
return new Url('entity.migration_group.list');
}
/**
* The submit handler for the confirm form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// Delete the entity.
$this->entity->delete();
// Set a message that the entity was deleted.
$this->messenger()->addStatus($this->t('Migration group %label was deleted.', [
'%label' => $this->entity->label(),
]));
// Redirect the user to the list controller when complete.
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides the edit form for our Migration Group entity.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationGroupEditForm extends MigrationGroupFormBase {
/**
* Returns the actions provided by this form.
*
* For the edit form, we only need to change the text of the submit button.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*
* @return array
* An array of supported actions for the current entity form.
*/
public function actions(array $form, FormStateInterface $form_state): array {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Update Migration Group');
return $actions;
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
/**
* Base form for migration groups.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationGroupFormBase extends EntityForm {
/**
* Overrides Drupal\Core\Entity\EntityFormController::form().
*
* Builds the entity add/edit form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*
* @return array
* An associative array containing the migration group add/edit form.
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
// Get anything we need from the base class.
$form = parent::buildForm($form, $form_state);
/** @var \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group */
$migration_group = $this->entity;
// Build the form.
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $migration_group->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#title' => $this->t('Machine name'),
'#default_value' => $migration_group->id(),
'#machine_name' => [
'exists' => [$this, 'exists'],
'replace_pattern' => '([^a-z0-9_]+)|(^custom$)',
'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".',
],
'#disabled' => !$migration_group->isNew(),
];
$form['description'] = [
'#type' => 'textfield',
'#title' => $this->t('Description'),
'#maxlength' => 255,
'#default_value' => $migration_group->get('description'),
];
$form['source_type'] = [
'#type' => 'textfield',
'#title' => $this->t('Source type'),
'#description' => $this->t('Type of source system the group is migrating from, for example "Drupal 6" or "WordPress 4".'),
'#maxlength' => 255,
'#default_value' => $migration_group->get('source_type'),
];
// Return the form.
return $form;
}
/**
* Checks for an existing migration group.
*
* @param string|int $entity_id
* The entity ID.
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return bool
* TRUE if this format already exists, FALSE otherwise.
*/
public function exists($entity_id, array $element, FormStateInterface $form_state): bool {
$query = $this->entityTypeManager->getStorage('migration_group')
->getQuery()
->accessCheck(TRUE);
// Query the entity ID to see if its in use.
$result = $query->condition('id', $element['#field_prefix'] . $entity_id)
->execute();
// We don't need to return the ID, only if it exists or not.
return (bool) $result;
}
/**
* Overrides Drupal\Core\Entity\EntityFormController::actions().
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An associative array containing the current state of the form.
*
* @return array
* An array of supported actions for the current entity form.
*/
protected function actions(array $form, FormStateInterface $form_state): array {
// Get the basic actions from the base class.
$actions = parent::actions($form, $form_state);
// Change the submit button text.
$actions['submit']['#value'] = $this->t('Save');
// Return the result.
return $actions;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state): void {
$migration_group = $this->getEntity();
$status = $migration_group->save();
if ($status == SAVED_UPDATED) {
// If we edited an existing entity...
$this->messenger()->addStatus($this->t('Migration group %label has been updated.', ['%label' => $migration_group->label()]));
}
else {
// If we created a new entity...
$this->messenger()->addStatus($this->t('Migration group %label has been added.', ['%label' => $migration_group->label()]));
}
// Redirect the user back to the listing route after the save operation.
$form_state->setRedirect('entity.migration_group.list');
}
}

View File

@@ -0,0 +1,273 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Class to filter ID map by an ID list.
*/
class IdMapFilter extends \FilterIterator implements MigrateIdMapInterface {
/**
* List of specific source IDs to import.
*/
protected array $idList;
/**
* IdMapFilter constructor.
*
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map
* The ID map.
* @param array $id_list
* The id list to use in the filter.
*/
public function __construct(MigrateIdMapInterface $id_map, array $id_list) {
parent::__construct($id_map);
$this->idList = $id_list;
}
/**
* {@inheritdoc}
*/
public function accept(): bool {
// Row is included.
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
if (empty($this->idList) || in_array(array_values($this->currentSource()), $this->idList)) {
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function saveIdMapping(Row $row, array $destination_id_values, $status = self::STATUS_IMPORTED, $rollback_action = self::ROLLBACK_DELETE): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->saveIdMapping($row, $destination_id_values, $status, $rollback_action);
}
/**
* {@inheritdoc}
*/
public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->saveMessage($source_id_values, $message, $level);
}
/**
* {@inheritdoc}
*/
public function getMessages(array $source_id_values = [], $level = NULL): \Traversable {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->getMessages($source_id_values, $level);
}
/**
* {@inheritdoc}
*/
public function prepareUpdate(): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->prepareUpdate();
}
/**
* {@inheritdoc}
*/
public function processedCount(): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->processedCount();
}
/**
* {@inheritdoc}
*/
public function importedCount(): int {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->importedCount();
}
/**
* {@inheritdoc}
*/
public function updateCount(): int {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->updateCount();
}
/**
* {@inheritdoc}
*/
public function errorCount(): int {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->errorCount();
}
/**
* {@inheritdoc}
*/
public function messageCount(): int {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->messageCount();
}
/**
* {@inheritdoc}
*/
public function delete(array $source_id_values, $messages_only = FALSE): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->delete($source_id_values, $messages_only);
}
/**
* {@inheritdoc}
*/
public function deleteDestination(array $destination_id_values): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->deleteDestination($destination_id_values);
}
/**
* {@inheritdoc}
*/
public function clearMessages(): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->clearMessages();
}
/**
* {@inheritdoc}
*/
public function getRowBySource(array $source_id_values): array {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->getRowBySource($source_id_values);
}
/**
* {@inheritdoc}
*/
public function getRowByDestination(array $destination_id_values): array {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->getRowByDestination($destination_id_values);
}
/**
* {@inheritdoc}
*/
public function getRowsNeedingUpdate($count): array {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->getRowsNeedingUpdate($count);
}
/**
* {@inheritdoc}
*/
public function lookupSourceId(array $destination_id_values): array {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->lookupSourceId($destination_id_values);
}
/**
* {@inheritdoc}
*/
public function lookupDestinationIds(array $source_id_values): array {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->lookupDestinationIds($source_id_values);
}
/**
* {@inheritdoc}
*/
public function currentDestination(): array {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->currentDestination();
}
/**
* {@inheritdoc}
*/
public function currentSource(): array {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->currentSource() ?? [];
}
/**
* {@inheritdoc}
*/
public function destroy(): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->destroy();
}
/**
* {@inheritdoc}
*/
public function getQualifiedMapTableName(): string {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->getQualifiedMapTableName();
}
/**
* {@inheritdoc}
*/
public function setMessage(MigrateMessageInterface $message): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->setMessage($message);
}
/**
* {@inheritdoc}
*/
public function setUpdate(array $source_id_values): void {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
$map->setUpdate($source_id_values);
}
/**
* {@inheritdoc}
*/
public function getPluginId(): string {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->getPluginId();
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition(): array {
$map = $this->getInnerIterator();
\assert($map instanceof MigrateIdMapInterface);
return $map->getPluginDefinition();
}
}

View File

@@ -0,0 +1,331 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\migrate\MigrateMessage;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
/**
* Defines a migrate executable class for batch migrations through UI.
*/
class MigrateBatchExecutable extends MigrateExecutable {
/**
* Representing a batch import operation.
*/
public const BATCH_IMPORT = 1;
/**
* Indicates if we need to update existing rows or skip them.
*
* @var int
*/
protected int $updateExistingRows = 0;
/**
* Indicates if we need import dependent migrations also.
*
* @var int
*/
protected int $checkDependencies = 0;
/**
* The ID list as single string expression.
*
* @var string
*/
protected string $idlistExpression = '';
protected bool $syncSource = FALSE;
protected $batchContext;
protected array $configuration = [];
protected MigrationPluginManagerInterface $migrationPluginManager;
/**
* {@inheritdoc}
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, array $options = []) {
if (isset($options['update'])) {
$this->updateExistingRows = $options['update'];
}
if (isset($options['force'])) {
$this->checkDependencies = $options['force'];
}
if (isset($options['sync'])) {
$this->syncSource = $options['sync'];
}
if (isset($options['configuration'])) {
$this->configuration = $options['configuration'];
}
if (isset($options['idlist'])) {
$this->idlistExpression = $options['idlist'];
}
parent::__construct($migration, $message, $options);
$this->migrationPluginManager = \Drupal::getContainer()->get('plugin.manager.migration');
}
/**
* Sets the current batch content so listeners can update the messages.
*
* @param array|\DrushBatchContext $context
* The batch context.
*/
public function setBatchContext(&$context): void {
$this->batchContext = &$context;
}
/**
* Gets a reference to the current batch context.
*
* The batch context.
*/
public function &getBatchContext() {
return $this->batchContext;
}
/**
* Setup batch operations for running the migration.
*/
public function batchImport(): void {
// Create the batch operations for each migration that needs to be executed.
// This includes the migration for this executable, but also the dependent
// migrations.
$operations = $this->batchOperations([$this->migration], 'import', [
'limit' => $this->itemLimit,
'update' => $this->updateExistingRows,
'force' => $this->checkDependencies,
'sync' => $this->syncSource,
'idlist' => $this->idlistExpression ?: NULL,
'configuration' => $this->configuration,
]);
if (count($operations) > 0) {
$batch = [
'operations' => $operations,
'title' => $this->t('Migrating %migrate', ['%migrate' => $this->migration->label()]),
'init_message' => $this->t('Start migrating %migrate', ['%migrate' => $this->migration->label()]),
'progress_message' => $this->t('Migrating %migrate', ['%migrate' => $this->migration->label()]),
'error_message' => $this->t('An error occurred while migrating %migrate.', ['%migrate' => $this->migration->label()]),
'finished' => [static::class, 'batchFinishedImport'],
];
batch_set($batch);
}
}
/**
* Helper to generate the batch operations for importing migrations.
*
* @param \Drupal\migrate\Plugin\MigrationInterface[] $migrations
* The migrations.
* @param string $operation
* The batch operation to perform.
* @param array $options
* The migration options.
*
* @return array
* The batch operations to perform.
*/
protected function batchOperations(array $migrations, string $operation, array $options = []): array {
$operations = [];
foreach ($migrations as $migration) {
if (!empty($options['update'])) {
if (empty($options['idlist'])) {
$migration->getIdMap()->prepareUpdate();
}
else {
$source_id_values_list = MigrateTools::buildIdList($options);
$keys = array_keys($migration->getSourcePlugin()->getIds());
foreach ($source_id_values_list as $source_id_values) {
$migration->getIdMap()->setUpdate(array_combine($keys, $source_id_values));
}
}
}
if (!empty($options['force'])) {
$migration->set('requirements', []);
}
else {
$dependencies = $migration->getMigrationDependencies();
if (!empty($dependencies['required'])) {
$required_migrations = $this->migrationPluginManager->createInstances($dependencies['required']);
// For dependent migrations will need to be migrate all items.
$operations = array_merge($operations, $this->batchOperations($required_migrations, $operation, [
'limit' => 0,
'update' => $options['update'],
'force' => $options['force'],
'sync' => $options['sync'],
]));
}
}
$operations[] = [
sprintf('%s::%s', static::class, 'batchProcessImport'),
[$migration->id(), $options],
];
}
return $operations;
}
/**
* Batch 'operation' callback.
*
* @param string $migration_id
* The migration id.
* @param array $options
* The batch executable options.
* @param array|\DrushBatchContext $context
* The sandbox context.
*/
public static function batchProcessImport(string $migration_id, array $options, &$context): void {
if (empty($context['sandbox'])) {
$context['finished'] = 0;
$context['sandbox'] = [];
$context['sandbox']['total'] = 0;
$context['sandbox']['counter'] = 0;
$context['sandbox']['batch_limit'] = 0;
$context['sandbox']['operation'] = self::BATCH_IMPORT;
}
// Prepare the migration executable.
$message = new MigrateMessage();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = \Drupal::getContainer()->get('plugin.manager.migration')->createInstance($migration_id, $options['configuration'] ?? []);
unset($options['configuration']);
// Each batch run we need to reinitialize the counter for the migration.
if (!empty($options['limit']) && isset($context['results'][$migration->id()]['@numitems'])) {
$options['limit'] -= $context['results'][$migration->id()]['@numitems'];
}
$executable = new static($migration, $message, $options);
if (empty($context['sandbox']['total'])) {
$context['sandbox']['total'] = $executable->getSource()->count();
$context['sandbox']['batch_limit'] = $executable->calculateBatchLimit($context);
$context['results'][$migration->id()] = [
'@numitems' => 0,
'@created' => 0,
'@updated' => 0,
'@failures' => 0,
'@ignored' => 0,
'@name' => $migration->id(),
];
}
// Every iteration, we reset our batch counter.
$context['sandbox']['batch_counter'] = 0;
// Make sure we know our batch context.
$executable->setBatchContext($context);
// Do the import.
$result = $executable->import();
// Store the result; will need to combine the results of all our iterations.
$context['results'][$migration->id()] = [
'@numitems' => $context['results'][$migration->id()]['@numitems'] + $executable->getProcessedCount(),
'@created' => $context['results'][$migration->id()]['@created'] + $executable->getCreatedCount(),
'@updated' => $context['results'][$migration->id()]['@updated'] + $executable->getUpdatedCount(),
'@failures' => $context['results'][$migration->id()]['@failures'] + $executable->getFailedCount(),
'@ignored' => $context['results'][$migration->id()]['@ignored'] + $executable->getIgnoredCount(),
'@name' => $migration->id(),
];
// Do some housekeeping.
if ($result !== MigrationInterface::RESULT_INCOMPLETE) {
$context['finished'] = 1;
}
else {
$context['sandbox']['counter'] = $context['results'][$migration->id()]['@numitems'];
if ($context['sandbox']['counter'] <= $context['sandbox']['total']) {
$context['finished'] = ((float) $context['sandbox']['counter'] / (float) $context['sandbox']['total']);
$context['message'] = t('Importing %migration (@percent%).', [
'%migration' => $migration->label(),
'@percent' => (int) ($context['finished'] * 100),
]);
}
}
}
/**
* Finished callback for import batches.
*
* @param bool $success
* A boolean indicating whether the batch has completed successfully.
* @param array $results
* The value set in $context['results'] by callback_batch_operation().
* @param array $operations
* If $success is FALSE, contains the operations that remained unprocessed.
*/
public static function batchFinishedImport(bool $success, array $results, array $operations): void {
if ($success) {
foreach ($results as $migration_id => $result) {
$singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'";
$plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'";
\Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural($result['@numitems'],
$singular_message,
$plural_message,
$result));
}
}
}
/**
* {@inheritdoc}
*/
public function checkStatus(): int {
$status = parent::checkStatus();
if ($status === MigrationInterface::RESULT_COMPLETED) {
// Do some batch housekeeping.
$context = $this->getBatchContext();
if (!empty($context['sandbox']) && $context['sandbox']['operation'] === self::BATCH_IMPORT) {
$context['sandbox']['batch_counter']++;
if ($context['sandbox']['batch_counter'] >= $context['sandbox']['batch_limit']) {
$status = MigrationInterface::RESULT_INCOMPLETE;
}
}
}
return $status;
}
/**
* Calculates how much a single batch iteration will handle.
*
* @param array|\DrushBatchContext $context
* The sandbox context.
*
* @return float
* The batch limit.
*/
public function calculateBatchLimit($context): float {
// @todo Maybe we need some other more sophisticated logic here?
return ceil($context['sandbox']['total'] / 100);
}
/**
* Suppress progress messages since we are executing via batch UI.
*
* @param bool $done
* TRUE if this is the last items to process. Otherwise FALSE.
*/
protected function progressMessage($done = TRUE) {}
}

View File

@@ -0,0 +1,450 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigrateMapDeleteEvent;
use Drupal\migrate\Event\MigrateMapSaveEvent;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\Event\MigrateRowDeleteEvent;
use Drupal\migrate\MigrateExecutable as MigrateExecutableBase;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_plus\Event\MigrateEvents as MigratePlusEvents;
use Drupal\migrate_plus\Event\MigratePrepareRowEvent;
/**
* Defines a migrate executable class for drush.
*/
class MigrateExecutable extends MigrateExecutableBase {
/**
* Counters of map statuses.
*
* Set of counters, keyed by MigrateIdMapInterface::STATUS_* constant.
*/
protected array $saveCounters = [
MigrateIdMapInterface::STATUS_FAILED => 0,
MigrateIdMapInterface::STATUS_IGNORED => 0,
MigrateIdMapInterface::STATUS_IMPORTED => 0,
MigrateIdMapInterface::STATUS_NEEDS_UPDATE => 0,
];
/**
* Counter of map saves, used to detect the item limit threshold.
*
* @var int
*/
protected $itemLimitCounter = 0;
/**
* Counter of map deletions.
*/
protected int $deleteCounter = 0;
/**
* Maximum number of items to process in this migration.
*
* 0 indicates no limit is to be applied.
*
* @var int
*/
protected $itemLimit = 0;
/**
* Frequency (in items) at which progress messages should be emitted.
*
* @var int
*/
protected $feedback = 0;
/**
* List of specific source IDs to import.
*/
protected array $idlist = [];
/**
* Count of number of items processed so far in this migration.
*
* @var int
*/
protected $counter = 0;
/**
* Whether the destination item exists before saving.
*/
protected bool $preExistingItem = FALSE;
/**
* List of event listeners we have registered.
*/
protected $listeners = [];
/**
* {@inheritdoc}
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message = NULL, array $options = []) {
parent::__construct($migration, $message);
if (isset($options['limit'])) {
$this->itemLimit = $options['limit'];
}
if (isset($options['feedback'])) {
$this->feedback = $options['feedback'];
}
if (isset($options['sync'])) {
$this->migration->set('syncSource', $options['sync']);
}
$this->idlist = MigrateTools::buildIdList($options);
$this->listeners[MigrateEvents::MAP_SAVE] = [
$this,
'onMapSave',
];
$this->listeners[MigrateEvents::MAP_DELETE] = [
$this,
'onMapDelete',
];
$this->listeners[MigrateEvents::POST_IMPORT] = [
$this,
'onPostImport',
];
$this->listeners[MigrateEvents::POST_ROLLBACK] = [
$this,
'onPostRollback',
];
$this->listeners[MigrateEvents::PRE_ROW_SAVE] = [
$this,
'onPreRowSave',
];
$this->listeners[MigrateEvents::POST_ROW_DELETE] = [
$this,
'onPostRowDelete',
];
if (class_exists(MigratePlusEvents::class)) {
$this->listeners[MigratePlusEvents::PREPARE_ROW] = [
$this,
'onPrepareRow',
];
}
foreach ($this->listeners as $event => $listener) {
$this->resetListeners($event);
$this->getEventDispatcher()->addListener($event, $listener);
}
}
/**
* Count up any map save events.
*
* @param \Drupal\migrate\Event\MigrateMapSaveEvent $event
* The map event.
*/
public function onMapSave(MigrateMapSaveEvent $event) {
// Only count saves for this migration.
if ($event->getMap()->getQualifiedMapTableName() == $this->migration->getIdMap()->getQualifiedMapTableName()) {
$fields = $event->getFields();
$this->itemLimitCounter++;
// Distinguish between creation and update.
if ($fields['source_row_status'] == MigrateIdMapInterface::STATUS_IMPORTED &&
$this->preExistingItem
) {
$this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]++;
}
else {
$this->saveCounters[$fields['source_row_status']]++;
}
}
}
/**
* Count up any rollback events.
*
* @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event
* The map event.
*/
public function onMapDelete(MigrateMapDeleteEvent $event) {
$this->deleteCounter++;
}
/**
* Return the number of items created.
*
* @return int
* The number of items created.
*/
public function getCreatedCount() {
return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED];
}
/**
* Return the number of items updated.
*
* @return int
* The updated count.
*/
public function getUpdatedCount() {
return $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE];
}
/**
* Return the number of items ignored.
*
* @return int
* The ignored count.
*/
public function getIgnoredCount() {
return $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED];
}
/**
* Return the number of items that failed.
*
* @return int
* The failed count.
*/
public function getFailedCount() {
return $this->saveCounters[MigrateIdMapInterface::STATUS_FAILED];
}
/**
* Return the total number of items processed.
*
* Note that STATUS_NEEDS_UPDATE is not counted, since this is typically set
* on stubs created as side effects, not on the primary item being imported.
*
* @return int
* The processed count.
*/
public function getProcessedCount() {
return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED] +
$this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE] +
$this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED] +
$this->saveCounters[MigrateIdMapInterface::STATUS_FAILED];
}
/**
* Return the number of items rolled back.
*
* @return int
* The rollback count.
*/
public function getRollbackCount() {
return $this->deleteCounter;
}
/**
* Reset all the per-status counters to 0.
*/
protected function resetCounters() {
foreach ($this->saveCounters as $status => $count) {
$this->saveCounters[$status] = 0;
}
$this->deleteCounter = 0;
}
/**
* React to migration completion.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The map event.
*/
public function onPostImport(MigrateImportEvent $event) {
$migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported');
$migrate_last_imported_store->set($event->getMigration()->id(), round(\Drupal::time()->getCurrentMicroTime() * 1000));
$this->progressMessage();
$this->removeListeners();
$unused_ids = $this->getSource()->getRemainingIdList();
if ($unused_ids) {
$this->message->display($this->t("The following specified IDs were not found in the source IDs: @idlist.", [
'@idlist' => implode(', ', array_map(static fn($ids): string => implode(':', $ids), $unused_ids)),
]));
}
}
/**
* Clean up all our event listeners.
*/
protected function removeListeners() {
foreach ($this->listeners as $event => $listener) {
// Don't remove the listener for the events that are currently being
// dispatched.
if ($event !== MigrateEvents::POST_IMPORT && $event !== MigrateEvents::POST_ROLLBACK) {
$this->getEventDispatcher()->removeListener($event, $listener);
}
}
}
/**
* Clean up the event listeners that cannot be removed by removeListeners().
*
* @param string $event_name
* The name of the event to remove.
*/
protected function resetListeners(string $event_name) {
if (in_array($event_name, [
MigrateEvents::POST_IMPORT,
MigrateEvents::POST_ROLLBACK,
], TRUE)) {
foreach ($this->getEventDispatcher()->getListeners($event_name) as $registered_listener) {
if ($registered_listener[0] instanceof self) {
$this->getEventDispatcher()->removeListener($event_name, $registered_listener);
}
}
}
}
/**
* Emit information on what we've done.
*
* Either since the last feedback or the beginning of this migration.
*
* @param bool $done
* TRUE if this is the last items to process. Otherwise FALSE.
*/
protected function progressMessage($done = TRUE) {
$processed = $this->getProcessedCount();
if ($done) {
$singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'";
$plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'";
}
else {
$singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - continuing with '@name'";
$plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - continuing with '@name'";
}
$this->message->display(\Drupal::translation()->formatPlural($processed,
$singular_message, $plural_message,
[
'@numitems' => $processed,
'@created' => $this->getCreatedCount(),
'@updated' => $this->getUpdatedCount(),
'@failures' => $this->getFailedCount(),
'@ignored' => $this->getIgnoredCount(),
'@name' => $this->migration->id(),
]
));
}
/**
* React to rollback completion.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The map event.
*/
public function onPostRollback(MigrateRollbackEvent $event) {
$migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported');
$migrate_last_imported_store->set($event->getMigration()->id(), FALSE);
$this->rollbackMessage();
// If this is a sync import, then don't remove listeners or post import will
// not be executed. Leave it to post import to remove listeners.
if (empty($event->getMigration()->syncSource)) {
$this->removeListeners();
}
}
/**
* Emit information on what we've done.
*
* Either since the last feedback or the beginning of this migration.
*
* @param bool $done
* TRUE if this is the last items to rollback. Otherwise FALSE.
*/
protected function rollbackMessage($done = TRUE) {
$translation = \Drupal::translation();
if (($rolled_back = $this->getRollbackCount()) === 0) {
$this->message->display($translation->translate(
"No item has been rolled back - done with '@name'",
['@name' => $this->migration->id()])
);
return;
}
if ($done) {
$singular_message = "Rolled back 1 item - done with '@name'";
$plural_message = "Rolled back @numitems items - done with '@name'";
}
else {
$singular_message = "Rolled back 1 item - continuing with '@name'";
$plural_message = "Rolled back @numitems items - continuing with '@name'";
}
$this->message->display($translation->formatPlural($rolled_back,
$singular_message, $plural_message,
[
'@numitems' => $rolled_back,
'@name' => $this->migration->id(),
]
));
}
/**
* React to an item about to be imported.
*
* @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event
* The pre-save event.
*/
public function onPreRowSave(MigratePreRowSaveEvent $event) {
$id_map = $event->getRow()->getIdMap();
if (!empty($id_map['destid1'])) {
$this->preExistingItem = TRUE;
}
else {
$this->preExistingItem = FALSE;
}
}
/**
* React to item rollback.
*
* @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event
* The post-save event.
*/
public function onPostRowDelete(MigrateRowDeleteEvent $event) {
if ($this->feedback && ($this->deleteCounter) && $this->deleteCounter % $this->feedback == 0) {
$this->rollbackMessage(FALSE);
$this->resetCounters();
}
}
/**
* React to a new row.
*
* @param \Drupal\migrate_plus\Event\MigratePrepareRowEvent $event
* The prepare-row event.
*
* @throws \Drupal\migrate\MigrateSkipRowException
*/
public function onPrepareRow(MigratePrepareRowEvent $event) {
if ($this->feedback && $this->counter && $this->counter % $this->feedback == 0) {
$this->progressMessage(FALSE);
$this->resetCounters();
}
$this->counter++;
if ($this->itemLimit && ($this->itemLimitCounter + 1) >= $this->itemLimit) {
$event->getMigration()->interruptMigration(MigrationInterface::RESULT_COMPLETED);
}
}
/**
* {@inheritdoc}
*/
protected function getSource() {
if (!isset($this->source)) {
// Re-set $this->source which the call to the parent will have set.
$this->source = new SourceFilter(parent::getSource(), $this->idlist);
}
return $this->source;
}
/**
* {@inheritdoc}
*/
protected function getIdMap(): IdMapFilter {
return new IdMapFilter(parent::getIdMap(), $this->idlist);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\NestedArray;
/**
* Merged included shared migrate configuration.
*/
final class MigrateIncludeHandler {
private PluginManagerInterface $sharedConfiguration;
public function __construct(PluginManagerInterface $shared_config) {
$this->sharedConfiguration = $shared_config;
}
/**
* Include the shared configuration.
*/
public function include(array &$migration): void {
// Handle one or multiple includes.
$includes = (array) $migration['include'];
foreach ($includes as $include) {
$definition = $this->sharedConfiguration->getDefinition($include);
// Remove the shared configuration plugin metadata.
unset($definition['id'], $definition['class'], $definition['provider']);
$migration = NestedArray::mergeDeep($definition, $migration);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\Core\Plugin\PluginBase;
/**
* Default class used for migrate_shared_configuration plugins.
*/
final class MigrateSharedConfigDefault extends PluginBase implements MigrateSharedConfigInterface {
/**
* {@inheritdoc}
*/
public function id(): string {
return $this->pluginDefinition['id'];
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
/**
* Interface for migrate_shared_configuration plugins.
*/
interface MigrateSharedConfigInterface {
/**
* Returns the ID.
*
* @return string
* The shared configuration ID.
*/
public function id(): string;
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\migrate_tools\Discovery\YamlDiscoveryDecorator;
/**
* Defines a plugin manager to deal with migrate_shared_configuration.
*
* Modules can define migrate_shared_configuration in a
* MODULE_NAME.migrate_shared_configuration.yml file contained in the module's
* base directory. The migrate_shared_configuration has the following structure:
*
* @code
* MACHINE_NAME:
* source:
* key: drupal7
* MACHINE_NAME_2:
* source:
* batch_size: 1000
* @endcode
*
* Where everything besides MACHINE_NAME is the shared configuration.
*
* @see \Drupal\migrate_tools\MigrateSharedConfigDefault
* @see \Drupal\migrate_tools\MigrateSharedConfigInterface
* @see plugin_api
*/
final class MigrateSharedConfigPluginManager extends DefaultPluginManager {
/**
* {@inheritdoc}
*/
protected $defaults = [
// The migrate_shared_configuration id. Set by the plugin system based on
// the top-level YAML key.
'id' => '',
// Default plugin class.
'class' => MigrateSharedConfigDefault::class,
];
public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend) {
$this->factory = new ContainerFactory($this);
$this->moduleHandler = $module_handler;
$this->alterInfo('migrate_shared_configuration_info');
$this->setCacheBackend($cache_backend, 'migrate_shared_configuration_plugins');
}
/**
* {@inheritdoc}
*/
protected function getDiscovery(): DiscoveryInterface {
if (!isset($this->discovery)) {
// @todo Remove this in 7.0.0.
$old_discovery = new YamlDiscovery('migrate_shared_configuration', $this->moduleHandler->getModuleDirectories());
$directories = array_map(function ($directory) {
return [$directory . '/migrate_shared_configuration'];
}, $this->moduleHandler->getModuleDirectories());
$this->discovery = new YamlDiscoveryDecorator($old_discovery, $directories, 'migrate_shared_configuration');
}
return $this->discovery;
}
}

View File

@@ -0,0 +1,223 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\Core\Database\Connection;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Utility functionality for use in migrate_tools.
*/
class MigrateTools {
/**
* Default ID list delimiter.
*/
public const DEFAULT_ID_LIST_DELIMITER = ':';
/**
* Maximum number of source IDs to keep in memory before flushing them
* to database.
*/
protected const MAX_BUFFERED_SYNC_SOURCE_IDS_ENTRIES = 1000;
/**
* Sync Ids buffered in RAM before flushing them to database.
*/
protected array $bufferedSyncIdsEntries = [];
/**
* Array keeping track of migrations being in the Syncing IDs phase.
* Structure: List of `string => bool` where the key is the migration ID and
* the value is a boolean indicating whether it is currently syncing.
*/
protected array $syncingMigrations = [];
/**
* Connection to the database.
*/
public Connection $connection;
/**
* MigrateTools constructor.
*
* @param Connection $connection
* Connection to the database.
*/
public function __construct(Connection $connection) {
$this->connection = $connection;
}
/**
* Build the list of specific source IDs to import.
*
* @param array $options
* The migration executable options.
*
* The ID list.
*/
public static function buildIdList(array $options): array {
$options += [
'idlist' => NULL,
'idlist-delimiter' => self::DEFAULT_ID_LIST_DELIMITER,
];
$id_list = [];
if (is_scalar($options['idlist'])) {
$id_list = explode(',', (string) $options['idlist']);
array_walk($id_list, function (&$value) use ($options): void {
$value = str_getcsv($value, $options['idlist-delimiter']);
});
}
return $id_list;
}
/**
* Clears all SyncSourceIds entries from the database, for given migration.
*
* @param string $migrationId
* Migration ID.
*/
public function clearSyncSourceIds(string $migrationId): void
{
$query = $this->connection->delete('migrate_tools_sync_source_ids')
->condition('migration_id', $migrationId);
$query->execute();
}
/**
* Adds a SyncSourceIds entry to the database, for given migration.
*
* @param string $migrationId
* Migration ID.
* @param array $sourceIds
* A set of SyncSourceIds. Gets serialized to retain its structure.
*
* @throws \Exception
*/
public function addToSyncSourceIds(string $migrationId, array $sourceIds): void
{
$this->bufferedSyncIdsEntries[] = [
'migration_id' => $migrationId,
// Serialize source IDs before saving them to retain their structure.
'source_ids' => serialize($sourceIds),
];
if (count($this->bufferedSyncIdsEntries) >= static::MAX_BUFFERED_SYNC_SOURCE_IDS_ENTRIES) {
$this->flushSyncSourceIdsToDatabase();
}
}
/**
* Flushes any pending SyncSourceIds to the database.
*
* @throws \Exception
*/
protected function flushSyncSourceIdsToDatabase(): void {
if (empty($this->bufferedSyncIdsEntries)) {
// Nothing to flush, do nothing.
return;
}
// Batch insert all buffered pending entries.
$query = $this->connection->insert('migrate_tools_sync_source_ids')
->fields(['migration_id', 'source_ids']);
foreach($this->bufferedSyncIdsEntries as $entry) {
$query->values($entry);
}
$query->execute();
// Clear buffered pending entries.
$this->bufferedSyncIdsEntries = [];
}
/**
* Returns all SyncSourceIds from the database, for given migration.
*
* @param string $migrationId
* Migration ID.
*
* @return array
* Ids, structured as they were inserted.
*
* @throws \Exception
*/
public function getSyncSourceIds(string $migrationId): array
{
// Ensure all data was flushed to database before retrieving all of them.
$this->flushSyncSourceIdsToDatabase();
// Retrieve all IDs.
$serializedSourceIds = $this->connection->query(
'SELECT source_ids FROM {migrate_tools_sync_source_ids} WHERE migration_id = :mid',
[':mid' => $migrationId],
)
->fetchCol();
// Unserialize source IDs to restore their structure.
array_walk($serializedSourceIds, static function(&$entry) {
$entry = unserialize($entry);
});
return $serializedSourceIds;
}
/**
* Sets the syncing state of a migration.
*
* @param string $migrationId
* Migration ID.
* @param bool $isSyncing
* State to set.
*/
public function setMigrationSyncingState(string $migrationId, bool $isSyncing): void
{
$this->syncingMigrations[$migrationId] = $isSyncing;
}
/**
* Returns the syncing state of a migration.
*
* @param string $migrationId
* Migration ID.
*
* @return bool
* Whether the migration is currently syncing its IDs or not.
*/
public function isMigrationSyncing(string $migrationId): bool
{
return $this->syncingMigrations[$migrationId] ?? FALSE;
}
/**
* Returns a mapping of log levels to a human-friendly label.
*
* @return array
* An array of log level labels.
*/
public static function getLogLevelLabelMapping() {
return [
MigrationInterface::MESSAGE_ERROR => t('Error'),
MigrationInterface::MESSAGE_WARNING => t('Warning'),
MigrationInterface::MESSAGE_NOTICE => t('Notice'),
MigrationInterface::MESSAGE_INFORMATIONAL => t('Informational'),
];
}
/**
* Returns a mapping of status levels to a human-friendly label.
*
* @return array
* An array of migration status labels.
*/
public static function getStatusLevelLabelMapping() {
return [
MigrateIdMapInterface::STATUS_IMPORTED => t('Imported'),
MigrateIdMapInterface::STATUS_NEEDS_UPDATE => t('Pending'),
MigrateIdMapInterface::STATUS_IGNORED => t('Ignored'),
MigrateIdMapInterface::STATUS_FAILED => t('Failed'),
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
use Symfony\Component\Routing\Route;
/**
* Route processor to expand migrate_group.
*/
class RouteProcessor implements OutboundRouteProcessorInterface {
private EntityTypeManagerInterface $entityTypeManager;
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL): void {
if ($route->hasDefault('_migrate_group')) {
$parameters['migration_group'] = 'default';
if ($this->entityTypeManager->hasHandler('migration', 'storage')) {
$migration = $this->entityTypeManager
->getStorage('migration')
->load($parameters['migration']);
if (($migration !== NULL) && $group = $migration->get('migration_group')) {
$parameters['migration_group'] = $group;
}
}
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_tools;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
/**
* Class to filter source by an ID list.
*/
class SourceFilter extends \FilterIterator implements MigrateSourceInterface {
/**
* Whether to filter the source IDs.
*/
protected bool $filterSourceIds;
/**
* List of specific source IDs to import.
*
* The accept() method removes an item from this when it successfully filters
* a value.
*/
protected array $idList;
/**
* SourceFilter constructor.
*
* @param \Drupal\migrate\Plugin\MigrateSourceInterface $source
* The ID map.
* @param array $id_list
* The id list to use in the filter.
*/
public function __construct(MigrateSourceInterface $source, array $id_list) {
parent::__construct($source);
$this->idList = $id_list;
$this->filterSourceIds = !empty($this->idList);
}
/**
* {@inheritdoc}
*/
public function accept(): bool {
// No idlist filtering, don't filter.
if (!$this->filterSourceIds) {
return TRUE;
}
// Some source plugins do not extend SourcePluginBase. These cannot be
// filtered so warn and return all values.
if (!$this->getInnerIterator() instanceof SourcePluginBase) {
trigger_error(sprintf('The source plugin %s is not an instance of %s. Extend from %s to support idlist filtering.', $this->getInnerIterator()->getPluginId(), SourcePluginBase::class, SourcePluginBase::class));
return TRUE;
}
$id_list_key = \array_search(array_values($this->getInnerIterator()->getCurrentIds()), $this->idList);
if ($id_list_key !== FALSE) {
// Row is included.
unset($this->idList[$id_list_key]);
return TRUE;
}
return FALSE;
}
/**
* Gets the remaining ID list.
*
* An array of the IDs which were not used by the filter.
*/
public function getRemainingIdList(): array {
return $this->idList;
}
/**
* {@inheritdoc}
*/
public function fields() {
return $this->getInnerIterator()->fields();
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
return $this->getInnerIterator()->prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function __toString(): string {
return $this->getInnerIterator()->__toString();
}
/**
* {@inheritdoc}
*/
public function getIds() {
return $this->getInnerIterator()->getIds();
}
/**
* {@inheritdoc}
*/
public function getSourceModule() {
return $this->getInnerIterator()->getSourceModule();
}
/**
* {@inheritdoc}
*/
public function count(): int {
return $this->getInnerIterator()->count();
}
/**
* {@inheritdoc}
*/
public function getPluginId() {
return $this->getInnerIterator()->getPluginId();
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition() {
return $this->getInnerIterator()->getPluginDefinition();
}
}