migmag-1.0.x-dev/migmag_process/src/MigMagMigrateStub.php
migmag_process/src/MigMagMigrateStub.php
<?php
declare(strict_types=1);
namespace Drupal\migmag_process;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\MigrateStub;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* An alternative migrate stub service.
*
* This stub service is able to create stubs even for partial entity IDs.
*
* When an entity reference field is referencing node 17, and node 17 happens to
* be the French translation of node 13, then we need a stub to be created for
* node 13.
*
* Because entity references only identify the referenced entity by ID (the `id`
* entity key), and not by revision ID (the `revision` entity key) nor langcode
* (the `langcode` entity key), we need the ability to create stubs for those
* entity reference fields that will eventually resolve to a more specific
* entity — entity reference fields simply do not reference a particular
* revision nor a language.
*
* Note: the contrib entity_reference_revisions module does reference a
* particular revision!
*
* @see https://drupal.org/i/3156730
*/
class MigMagMigrateStub extends MigrateStub {
/**
* Static cache for identifying the main stub process.
*
* If this property is set to true, it means that the current process belongs
* to an entity being stubbed, so the service won't try to create the
* 'sub-stub' entity.
*
* @var bool
*/
protected static $mainStubProcessHasStarted = FALSE;
/**
* Creates a stub.
*
* @param string $migration_id
* The migration to stub.
* @param array $source_ids
* An array of source ids.
* @param array $default_values
* (optional) An array of default values to add to the stub.
* @param bool $key_by_destination_ids
* (optional) NULL or TRUE to force indexing of the return array by
* destination id keys (default), or FALSE to return the raw return value of
* the destination plugin's ::import() method. The return value from
* MigrateDestinationInterface::import() is very poorly defined as "The
* entity ID or an indication of success". In practice, the mapping systems
* expect and all destination plugins return an array of destination
* identifiers. Unfortunately these arrays are inconsistently keyed. The
* core destination plugins return a numerically indexed array of
* destination identifiers, but several contrib destinations return an array
* of identifiers indexed by the destination keys. This method will
* generally index all return arrays for consistency and to provide as much
* information as possible, but this parameter is added for backwards
* compatibility to allow accessing the original array.
* @param bool $create_only_valid
* (optional) Create stub only if the provided source IDs can be found in
* the source of the given migration. Defaults to FALSE.
*
* @return array|false
* An array of destination ids for the new stub, keyed by destination id
* key, or false if the stub failed.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\migrate\MigrateException
* @throws \LogicException
*/
public function createStub($migration_id, array $source_ids, array $default_values = [], $key_by_destination_ids = NULL, bool $create_only_valid = FALSE) {
if (!$create_only_valid) {
return parent::createStub($migration_id, $source_ids, $default_values, $key_by_destination_ids);
}
// If the main stub process was started, then this process would create a
// stub for an entity being stubbed.
// We will return FALSE.
if (self::$mainStubProcessHasStarted) {
return FALSE;
}
else {
self::$mainStubProcessHasStarted = TRUE;
}
$migrations = $this->migrationPluginManager->createInstances([$migration_id]);
if (!$migrations) {
throw new PluginNotFoundException($migration_id);
}
if (count($migrations) !== 1) {
throw new \LogicException(sprintf('Cannot stub derivable migration "%s". You must specify the id of a specific derivative to stub.', $migration_id));
}
// When we check non-sql sources for the matching row, we have to iterate
// over every row - which will delete previously recorded migration
// messages. So, at this phase we use a stub migration with "NULL" map
// plugin.
$definition = $this->migrationPluginManager->getDefinition($migration_id);
$definition['idMap'] = ['plugin' => 'null'];
$definition['source']['ignore_map'] = TRUE;
$migration = $this->migrationPluginManager->createStubMigration($definition);
$source_plugin = $migration->getSourcePlugin();
$source_plugin_ids = $source_plugin->getIds();
$source_id_keys = array_keys($source_plugin_ids);
$source_id_keys_with_aliases = [];
foreach ($source_plugin_ids as $key => $key_data) {
$full_key = empty($key_data['alias'])
? $key
: "{$key_data['alias']}.{$key}";
$source_id_keys_with_aliases[] = $full_key;
}
$source_ids = array_combine(array_slice($source_id_keys, 0, count($source_ids)), $source_ids);
$source_ids_with_aliases = array_combine(array_slice($source_id_keys_with_aliases, 0, count($source_ids)), $source_ids);
// Check the existence of a source that matches the source IDs before
// blindly create a stub.
$stub_should_be_created = FALSE;
$sql_succeed = $source_plugin instanceof SqlBase;
$rows_to_stub = [];
if (!$stub_should_be_created && $source_plugin instanceof SqlBase) {
$source_plugin_query = $source_plugin->query();
try {
foreach ($source_ids_with_aliases as $source_id => $source_id_value) {
$source_plugin_query->condition($source_id, $source_id_value);
}
$rows_to_stub = array_reduce($source_plugin_query->execute()->fetchAll(), function (array $carry, $source_data) use ($source_plugin, $migration) {
$row = new Row($source_data, $migration->getSourcePlugin()->getIds(), TRUE);
// Stubbing nodes with core node migrations needs prepareRow.
// Drupal core 8.9.x has a lot of source plugins which are returning
// void (NULL), so we have to strictly check for FALSE in case of
// skipped rows.
if ($source_plugin->prepareRow($row) !== FALSE) {
$carry[] = $row;
}
return $carry;
}, []);
$stub_should_be_created = (bool) count($rows_to_stub);
}
catch (DatabaseExceptionWrapper $e) {
$sql_succeed = FALSE;
}
}
// Cannot use the source plugin's query and condition, let's do the slower
// discovery.
if (!$stub_should_be_created && !$sql_succeed) {
try {
foreach ($source_plugin as $row) {
assert($row instanceof Row);
foreach ($source_ids_with_aliases as $source_id_key => $source_id_value) {
if ($row->getSourceProperty($source_id_key) !== $source_id_value) {
continue 2;
}
}
$stub_should_be_created = TRUE;
$rows_to_stub[] = $row;
}
}
catch (\Exception $e) {
}
}
$stubs = [];
if ($stub_should_be_created) {
// If stubs can (and should) be created, then we instantiate the migration
// again, this time using the original definition, with the original ID
// map plugin (so stubs will be tracked in by the actual ID map).
$migration = $this->migrationPluginManager->createInstance($migration_id);
assert($migration instanceof MigrationInterface);
// We will create stubs from every matching row.
foreach ($rows_to_stub as $row_to_stub) {
// @todo Needs core issue to fix upstream: the "status" field won't get
// populated (because it was not a field in D7). For now we work around
// this by passing every source row column as the set of default values,
// which will cause the status field to get populated.
assert($row_to_stub instanceof Row);
try {
if ($stub = $this->doCreateStub($migration, $row_to_stub->getSource(), $default_values)) {
$stubs[] = $stub;
}
}
// MigrateStub::doCreateStub() also throws MigrateSkipRowException.
// @todo Remove after https://www.drupal.org/i/3188455 is fixed.
catch (MigrateSkipRowException $e) {
}
catch (MigrateException $e) {
}
}
}
// If the return from ::import is numerically indexed, and we aren't
// requesting the raw return value, index it associatively using the
// destination id keys.
$stub = empty($stubs)
? FALSE
: reset($stubs);
if ($stub && ($key_by_destination_ids !== FALSE) && array_keys($stub) === range(0, count($stub) - 1)) {
$stub = array_combine(array_keys($migration->getDestinationPlugin()->getIds()), $stub);
}
// The main stub process finished, reset the static variable.
self::$mainStubProcessHasStarted = FALSE;
return $stub;
}
}
