Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,60 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Annotation\MigrateDestination.
*/
namespace Drupal\migrate\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a migration destination plugin annotation object.
*
* Plugin Namespace: Plugin\migrate\destination
*
* For a working example, see
* \Drupal\migrate\Plugin\migrate\destination\UrlAlias
*
* @see \Drupal\migrate\Plugin\MigrateDestinationInterface
* @see \Drupal\migrate\Plugin\destination\DestinationBase
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see plugin_api
*
* @ingroup migration
*
* @Annotation
*/
class MigrateDestination extends Plugin {
/**
* A unique identifier for the process plugin.
*
* @var string
*/
public $id;
/**
* Whether requirements are met.
*
* If TRUE and a 'provider' key is present in the annotation then the
* default destination plugin manager will set this to FALSE if the
* provider (module/theme) doesn't exist.
*
* @var bool
*/
public $requirements_met = TRUE;
/**
* A class to make the plugin derivative aware.
*
* @var string
*
* @see \Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator
*/
public $derivative;
}

View file

@ -0,0 +1,52 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Annotation\MigrateProcessPlugin.
*/
namespace Drupal\migrate\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a migration process plugin annotation object.
*
* Plugin Namespace: Plugin\migrate\process
*
* For a working example, see
* \Drupal\migrate\Plugin\migrate\process\DefaultValue
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
* @see \Drupal\migrate\ProcessPluginBase
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*
* @Annotation
*/
class MigrateProcessPlugin extends Plugin {
/**
* A unique identifier for the process plugin.
*
* @var string
*/
public $id;
/**
* Whether the plugin handles multiples itself.
*
* Typically these plugins will expect an array as input and iterate over it
* themselves, changing the whole array. For example the 'iterator' and the
* 'flatten' plugins. If the plugin only need to change a single value it
* can skip setting this attribute and let
* \Drupal\migrate\MigrateExecutable::processRow() handle the iteration.
*
* @var bool (optional)
*/
public $handle_multiples = FALSE;
}

View file

@ -0,0 +1,48 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Annotation\MigrateSource.
*/
namespace Drupal\migrate\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a migration source plugin annotation object.
*
* Plugin Namespace: Plugin\migrate\source
*
* For a working example, check
* \Drupal\migrate\Plugin\migrate\source\EmptySource
* \Drupal\migrate_drupal\Plugin\migrate\source\UrlAlias
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*
* @Annotation
*/
class MigrateSource extends Plugin {
/**
* A unique identifier for the process plugin.
*
* @var string
*/
public $id;
/**
* Whether requirements are met.
*
* @var bool
*/
public $requirements_met = TRUE;
}

View file

@ -0,0 +1,537 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Entity\Migration.
*/
namespace Drupal\migrate\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\RequirementsInterface;
use Drupal\Component\Utility\NestedArray;
/**
* Defines the Migration entity.
*
* The migration entity stores the information about a single migration, like
* the source, process and destination plugins.
*
* @ConfigEntityType(
* id = "migration",
* label = @Translation("Migration"),
* module = "migrate",
* handlers = {
* "storage" = "Drupal\migrate\MigrationStorage"
* },
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "weight" = "weight"
* }
* )
*/
class Migration extends ConfigEntityBase implements MigrationInterface, RequirementsInterface {
/**
* The migration ID (machine name).
*
* @var string
*/
protected $id;
/**
* The human-readable label for the migration.
*
* @var string
*/
protected $label;
/**
* The plugin ID for the row.
*
* @var string
*/
protected $row;
/**
* The source configuration, with at least a 'plugin' key.
*
* Used to initialize the $sourcePlugin.
*
* @var array
*/
protected $source;
/**
* The source plugin.
*
* @var \Drupal\migrate\Plugin\MigrateSourceInterface
*/
protected $sourcePlugin;
/**
* The configuration describing the process plugins.
*
* This is a strictly internal property and should not returned to calling
* code, use getProcess() instead.
*
* @var array
*/
protected $process;
/**
* The configuration describing the load plugins.
*
* @var array
*/
protected $load;
/**
* The cached process plugins.
*
* @var array
*/
protected $processPlugins = [];
/**
* The destination configuration, with at least a 'plugin' key.
*
* Used to initialize $destinationPlugin.
*
* @var array
*/
protected $destination;
/**
* The destination plugin.
*
* @var \Drupal\migrate\Plugin\MigrateDestinationInterface
*/
protected $destinationPlugin;
/**
* The identifier map data.
*
* Used to initialize $idMapPlugin.
*
* @var string
*/
protected $idMap = [];
/**
* The identifier map.
*
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $idMapPlugin;
/**
* The source identifiers.
*
* An array of source identifiers: the keys are the name of the properties,
* the values are dependent on the ID map plugin.
*
* @var array
*/
protected $sourceIds = [];
/**
* The destination identifiers.
*
* An array of destination identifiers: the keys are the name of the
* properties, the values are dependent on the ID map plugin.
*
* @var array
*/
protected $destinationIds = [];
/**
* Information on the high water mark.
*
* @var array
*/
protected $highWaterProperty;
/**
* Indicate whether the primary system of record for this migration is the
* source, or the destination (Drupal). In the source case, migration of
* an existing object will completely replace the Drupal object with data from
* the source side. In the destination case, the existing Drupal object will
* be loaded, then changes from the source applied; also, rollback will not be
* supported.
*
* @var string
*/
protected $systemOfRecord = self::SOURCE;
/**
* Specify value of source_row_status for current map row. Usually set by
* MigrateFieldHandler implementations.
*
* @var int
*/
protected $sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
/**
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $highWaterStorage;
/**
* Track time of last import if TRUE.
*
* @var bool
*/
protected $trackLastImported = FALSE;
/**
* These migrations must be already executed before this migration can run.
*
* @var array
*/
protected $requirements = [];
/**
* These migrations, if run, must be executed before this migration.
*
* These are different from the configuration dependencies. Migration
* dependencies are only used to store relationships between migrations.
*
* The migration_dependencies value is structured like this:
* @code
* array(
* 'required' => array(
* // An array of migration IDs that must be run before this migration.
* ),
* 'optional' => array(
* // An array of migration IDs that, if they exist, must be run before
* // this migration.
* ),
* );
* @endcode
*
* @var array
*/
protected $migration_dependencies = [];
/**
* The migration's configuration dependencies.
*
* These store any dependencies on modules or other configuration (including
* other migrations) that must be available before the migration can be
* created.
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
*
* @var array
*/
protected $dependencies = [];
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* {@inheritdoc}
*/
public function getSourcePlugin() {
if (!isset($this->sourcePlugin)) {
$this->sourcePlugin = \Drupal::service('plugin.manager.migrate.source')->createInstance($this->source['plugin'], $this->source, $this);
}
return $this->sourcePlugin;
}
/**
* {@inheritdoc}
*/
public function getProcessPlugins(array $process = NULL) {
if (!isset($process)) {
$process = $this->process;
}
$index = serialize($process);
if (!isset($this->processPlugins[$index])) {
$this->processPlugins[$index] = array();
foreach ($this->getProcessNormalized($process) as $property => $configurations) {
$this->processPlugins[$index][$property] = array();
foreach ($configurations as $configuration) {
if (isset($configuration['source'])) {
$this->processPlugins[$index][$property][] = \Drupal::service('plugin.manager.migrate.process')->createInstance('get', $configuration, $this);
}
// Get is already handled.
if ($configuration['plugin'] != 'get') {
$this->processPlugins[$index][$property][] = \Drupal::service('plugin.manager.migrate.process')->createInstance($configuration['plugin'], $configuration, $this);
}
if (!$this->processPlugins[$index][$property]) {
throw new MigrateException("Invalid process configuration for $property");
}
}
}
}
return $this->processPlugins[$index];
}
/**
* Resolve shorthands into a list of plugin configurations.
*
* @param array $process
* A process configuration array.
*
* @return array
* The normalized process configuration.
*/
protected function getProcessNormalized(array $process) {
$normalized_configurations = array();
foreach ($process as $destination => $configuration) {
if (is_string($configuration)) {
$configuration = array(
'plugin' => 'get',
'source' => $configuration,
);
}
if (isset($configuration['plugin'])) {
$configuration = array($configuration);
}
$normalized_configurations[$destination] = $configuration;
}
return $normalized_configurations;
}
/**
* {@inheritdoc}
*/
public function getDestinationPlugin($stub_being_requested = FALSE) {
if (!isset($this->destinationPlugin)) {
if ($stub_being_requested && !empty($this->destination['no_stub'])) {
throw new MigrateSkipRowException;
}
$this->destinationPlugin = \Drupal::service('plugin.manager.migrate.destination')->createInstance($this->destination['plugin'], $this->destination, $this);
}
return $this->destinationPlugin;
}
/**
* {@inheritdoc}
*/
public function getIdMap() {
if (!isset($this->idMapPlugin)) {
$configuration = $this->idMap;
$plugin = isset($configuration['plugin']) ? $configuration['plugin'] : 'sql';
$this->idMapPlugin = \Drupal::service('plugin.manager.migrate.id_map')->createInstance($plugin, $configuration, $this);
}
return $this->idMapPlugin;
}
/**
* Get the high water storage object.
*
* @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
* The storage object.
*/
protected function getHighWaterStorage() {
if (!isset($this->highWaterStorage)) {
$this->highWaterStorage = \Drupal::keyValue('migrate:high_water');
}
return $this->highWaterStorage;
}
/**
* {@inheritdoc}
*/
public function getHighWater() {
return $this->getHighWaterStorage()->get($this->id());
}
/**
* {@inheritdoc}
*/
public function saveHighWater($high_water) {
$this->getHighWaterStorage()->set($this->id(), $high_water);
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
// Check whether the current migration source and destination plugin
// requirements are met or not.
if ($this->getSourcePlugin() instanceof RequirementsInterface) {
$this->getSourcePlugin()->checkRequirements();
}
if ($this->getDestinationPlugin() instanceof RequirementsInterface) {
$this->getDestinationPlugin()->checkRequirements();
}
/** @var \Drupal\migrate\Entity\MigrationInterface[] $required_migrations */
$required_migrations = $this->getEntityManager()->getStorage('migration')->loadMultiple($this->requirements);
$missing_migrations = array_diff($this->requirements, array_keys($required_migrations));
// Check if the dependencies are in good shape.
foreach ($required_migrations as $migration_id => $required_migration) {
if (!$required_migration->isComplete()) {
$missing_migrations[] = $migration_id;
}
}
if ($missing_migrations) {
throw new RequirementsException(SafeMarkup::format('Missing migrations @requirements.', ['@requirements' => implode(', ', $missing_migrations)]), ['requirements' => $missing_migrations]);
}
}
/**
* Get the entity manager.
*
* @return \Drupal\Core\Entity\EntityManagerInterface
* The entity manager.
*/
protected function getEntityManager() {
if (!isset($this->entityManager)) {
$this->entityManager = \Drupal::entityManager();
}
return $this->entityManager;
}
/**
* {@inheritdoc}
*/
public function setMigrationResult($result) {
$migrate_result_store = \Drupal::keyValue('migrate_result');
$migrate_result_store->set($this->id(), $result);
}
/**
* {@inheritdoc}
*/
public function getMigrationResult() {
$migrate_result_store = \Drupal::keyValue('migrate_result');
return $migrate_result_store->get($this->id(), static::RESULT_INCOMPLETE);
}
/**
* {@inheritdoc}
*/
public function isComplete() {
return $this->getMigrationResult() === static::RESULT_COMPLETED;
}
/**
* {@inheritdoc}
*/
public function set($property_name, $value) {
if ($property_name == 'source') {
// Invalidate the source plugin.
unset($this->sourcePlugin);
}
return parent::set($property_name, $value);
}
/**
* {@inheritdoc}
*/
public function getProcess() {
return $this->getProcessNormalized($this->process);
}
/**
* {@inheritdoc}
*/
public function setProcess(array $process) {
$this->process = $process;
return $this;
}
/**
* {@inheritdoc}
*/
public function setProcessOfProperty($property, $process_of_property) {
$this->process[$property] = $process_of_property;
return $this;
}
/**
* {@inheritdoc}
*/
public function mergeProcessOfProperty($property, array $process_of_property) {
// If we already have a process value then merge the incoming process array
//otherwise simply set it.
$current_process = $this->getProcess();
if (isset($current_process[$property])) {
$this->process = NestedArray::mergeDeepArray([$current_process, $this->getProcessNormalized([$property => $process_of_property])], TRUE);
}
else {
$this->setProcessOfProperty($property, $process_of_property);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getSystemOfRecord() {
return $this->systemOfRecord;
}
/**
* {@inheritdoc}
*/
public function setSystemOfRecord($system_of_record) {
$this->systemOfRecord = $system_of_record;
return $this;
}
/**
* {@inheritdoc}
*/
public function isTrackLastImported() {
return $this->trackLastImported;
}
/**
* {@inheritdoc}
*/
public function setTrackLastImported($track_last_imported) {
$this->trackLastImported = (bool) $track_last_imported;
return $this;
}
/**
* {@inheritdoc}
*/
public function getMigrationDependencies() {
return $this->migration_dependencies + ['required' => [], 'optional' => []];
}
/**
* {@inheritdoc}
*/
public function trustData() {
// Migrations cannot be trusted since they are often written by hand and not
// through a UI.
$this->trustedData = FALSE;
return $this;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
$this->calculatePluginDependencies($this->getSourcePlugin());
$this->calculatePluginDependencies($this->getDestinationPlugin());
// Add dependencies on required migration dependencies.
foreach ($this->getMigrationDependencies()['required'] as $dependency) {
$this->addDependency('config', $this->getEntityType()->getConfigPrefix() . '.' . $dependency);
}
return $this->dependencies;
}
}

View file

@ -0,0 +1,286 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Entity\MigrationInterface.
*/
namespace Drupal\migrate\Entity;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Interface for migrations.
*/
interface MigrationInterface extends ConfigEntityInterface {
/**
* A constant used for systemOfRecord.
*/
const SOURCE = 'source';
/**
* A constant used for systemOfRecord.
*/
const DESTINATION = 'destination';
/**
* The migration is currently not running.
*/
const STATUS_IDLE = 0;
/**
* The migration is currently importing.
*/
const STATUS_IMPORTING = 1;
/**
* The migration is currently being rolled back.
*/
const STATUS_ROLLING_BACK = 2;
/**
* The migration is being stopped.
*/
const STATUS_STOPPING = 3;
/**
* The migration has been disabled.
*/
const STATUS_DISABLED = 4;
/**
* Migration error.
*/
const MESSAGE_ERROR = 1;
/**
* Migration warning.
*/
const MESSAGE_WARNING = 2;
/**
* Migration notice.
*/
const MESSAGE_NOTICE = 3;
/**
* Migration info.
*/
const MESSAGE_INFORMATIONAL = 4;
/**
* All records have been processed.
*/
const RESULT_COMPLETED = 1;
/**
* The process has stopped itself (e.g., the memory limit is approaching).
*/
const RESULT_INCOMPLETE = 2;
/**
* The process was stopped externally (e.g., via drush migrate-stop).
*/
const RESULT_STOPPED = 3;
/**
* The process had a fatal error.
*/
const RESULT_FAILED = 4;
/**
* Dependencies are unfulfilled - skip the process.
*/
const RESULT_SKIPPED = 5;
/**
* This migration is disabled, skipping.
*/
const RESULT_DISABLED = 6;
/**
* Returns the initialized source plugin.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface
* The source plugin.
*/
public function getSourcePlugin();
/**
* Returns the process plugins.
*
* @param array $process
* A process configuration array.
*
* @return \Drupal\migrate\Plugin\MigrateProcessInterface[][]
* An associative array. The keys are the destination property names. Values
* are process pipelines. Each pipeline contains an array of plugins.
*/
public function getProcessPlugins(array $process = NULL);
/**
* Returns the initialized destination plugin.
*
* @param bool $stub_being_requested
* TRUE to indicate that this destination will be asked to construct a stub.
*
* @return \Drupal\migrate\Plugin\MigrateDestinationInterface
* The destination plugin.
*/
public function getDestinationPlugin($stub_being_requested = FALSE);
/**
* Returns the initialized id_map plugin.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The ID map.
*/
public function getIdMap();
/**
* The current value of the high water mark.
*
* The high water mark defines a timestamp stating the time the import was last
* run. If the mark is set, only content with a higher timestamp will be
* imported.
*
* @return int
* A Unix timestamp representing the high water mark.
*/
public function getHighWater();
/**
* Save the new high water mark.
*
* @param int $high_water
* The high water timestamp.
*/
public function saveHighWater($high_water);
/**
* Check if this migration is complete.
*
* @return bool
* TRUE if this migration is complete otherwise FALSE.
*/
public function isComplete();
/**
* Set the migration result.
*
* @param int $result
* One of the RESULT_* constants.
*/
public function setMigrationResult($result);
/**
* Get the current migration result.
*
* @return int
* The current migration result. Defaults to RESULT_INCOMPLETE.
*/
public function getMigrationResult();
/**
* Get the normalized process pipeline configuration describing the process
* plugins.
*
* The process configuration is always normalized. All shorthand processing
* will be expanded into their full representations.
*
* @see https://www.drupal.org/node/2129651#get-shorthand
*
* @return array
* The normalized configuration describing the process plugins.
*/
public function getProcess();
/**
* Allows you to override the entire process configuration.
*
* @param array $process
* The entire process pipeline configuration describing the process plugins.
*
* @return $this
*/
public function setProcess(array $process);
/**
* Set the process pipeline configuration for an individual destination field.
*
* This method allows you to set the process pipeline configuration for a
* single property within the full process pipeline configuration.
*
* @param string $property
* The property of which to set the process pipeline configuration.
* @param mixed $process_of_property
* The process pipeline configuration to be set for this property.
*
* @return $this
* The migration entity.
*/
public function setProcessOfProperty($property, $process_of_property);
/**
* Merge the process pipeline configuration for a single property.
*
* @param string $property
* The property of which to merge the passed in process pipeline
* configuration.
* @param array $process_of_property
* The process pipeline configuration to be merged with the existing process
* pipeline configuration.
*
* @return $this
* The migration entity.
*
* @see Drupal\migrate_drupal\Plugin\migrate\load\LoadEntity::processLinkField().
*/
public function mergeProcessOfProperty($property, array $process_of_property);
/**
* Get the current system of record of the migration.
*
* @return string
* The current system of record of the migration.
*/
public function getSystemOfRecord();
/**
* Set the system of record for the migration.
*
* @param string $system_of_record
* The system of record of the migration.
*
* @return $this
*/
public function setSystemOfRecord($system_of_record);
/**
* Checks if the migration should track time of last import.
*
* @return bool
* TRUE if the migration is tracking last import time.
*/
public function isTrackLastImported();
/**
* Set if the migration should track time of last import.
*
* @param bool $track_last_imported
* Boolean value to indicate if the migration should track last import time.
*
* @return $this
*/
public function setTrackLastImported($track_last_imported);
/**
* Get the dependencies for this migration.
*
* @return array
* The dependencies for this migrations.
*/
public function getMigrationDependencies();
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,627 @@
<?php
/**
* @file
* Contains \Drupal\migrate\MigrateExecutable.
*/
namespace Drupal\migrate;
use Drupal\Core\Utility\Error;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Defines a migrate executable class.
*/
class MigrateExecutable implements MigrateExecutableInterface {
use StringTranslationTrait;
/**
* The configuration of the migration to do.
*
* @var \Drupal\migrate\Entity\Migration
*/
protected $migration;
/**
* The number of successfully imported rows since feedback was given.
*
* @var int
*/
protected $successesSinceFeedback;
/**
* The number of rows that were successfully processed.
*
* @var int
*/
protected $totalSuccesses;
/**
* Status of one row.
*
* The value is a MigrateIdMapInterface::STATUS_* constant, for example:
* STATUS_IMPORTED.
*
* @var int
*/
protected $sourceRowStatus;
/**
* The number of rows processed.
*
* The total attempted, whether or not they were successful.
*
* @var int
*/
protected $totalProcessed;
/**
* The queued messages not yet saved.
*
* Each element in the array is an array with two keys:
* - 'message': The message string.
* - 'level': The level, a MigrationInterface::MESSAGE_* constant.
*
* @var array
*/
protected $queuedMessages = array();
/**
* The options that can be set when executing the migration.
*
* Values can be set for:
* - 'limit': Sets a time limit.
*
* @var array
*/
protected $options;
/**
* The PHP max_execution_time.
*
* @var int
*/
protected $maxExecTime;
/**
* The ratio of the memory limit at which an operation will be interrupted.
*
* @var float
*/
protected $memoryThreshold = 0.85;
/**
* The ratio of the time limit at which an operation will be interrupted.
*
* @var float
*/
public $timeThreshold = 0.90;
/**
* The time limit when executing the migration.
*
* @var array
*/
public $limit = array();
/**
* The configuration values of the source.
*
* @var array
*/
protected $sourceIdValues;
/**
* The number of rows processed since feedback was given.
*
* @var int
*/
protected $processedSinceFeedback = 0;
/**
* The PHP memory_limit expressed in bytes.
*
* @var int
*/
protected $memoryLimit;
/**
* The rollback action to be saved for the current row.
*
* @var int
*/
public $rollbackAction;
/**
* An array of counts. Initially used for cache hit/miss tracking.
*
* @var array
*/
protected $counts = array();
/**
* The maximum number of items to pass in a single call during a rollback.
*
* For use in bulkRollback(). Can be overridden in derived class constructor.
*
* @var int
*/
protected $rollbackBatchSize = 50;
/**
* The object currently being constructed.
*
* @var \stdClass
*/
protected $destinationValues;
/**
* The source.
*
* @var \Drupal\migrate\Plugin\MigrateSourceInterface
*/
protected $source;
/**
* The current data row retrieved from the source.
*
* @var \stdClass
*/
protected $sourceValues;
/**
* Constructs a MigrateExecutable and verifies and sets the memory limit.
*
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The migration to run.
* @param \Drupal\migrate\MigrateMessageInterface $message
* The message to record.
*
* @throws \Drupal\migrate\MigrateException
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message) {
$this->migration = $migration;
$this->message = $message;
$this->migration->getIdMap()->setMessage($message);
// Record the memory limit in bytes
$limit = trim(ini_get('memory_limit'));
if ($limit == '-1') {
$this->memoryLimit = PHP_INT_MAX;
}
else {
if (!is_numeric($limit)) {
$last = strtolower(substr($limit, -1));
switch ($last) {
case 'g':
$limit *= 1024;
case 'm':
$limit *= 1024;
case 'k':
$limit *= 1024;
break;
default:
throw new MigrateException($this->t('Invalid PHP memory_limit !limit',
array('!limit' => $limit)));
}
}
$this->memoryLimit = $limit;
}
// Record the maximum execution time limit.
$this->maxExecTime = ini_get('max_execution_time');
}
/**
* Returns the source.
*
* Makes sure source is initialized based on migration settings.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface
* The source.
*/
protected function getSource() {
if (!isset($this->source)) {
$this->source = $this->migration->getSourcePlugin();
// @TODO, find out how to remove this.
// @see https://www.drupal.org/node/2443617
$this->source->migrateExecutable = $this;
}
return $this->source;
}
/**
* {@inheritdoc}
*/
public function import() {
// Knock off migration if the requirements haven't been met.
try {
$this->migration->checkRequirements();
}
catch (RequirementsException $e) {
$this->message->display(
$this->t('Migration @id did not meet the requirements. @message @requirements', array(
'@id' => $this->migration->id(),
'@message' => $e->getMessage(),
'@requirements' => $e->getRequirementsString(),
)), 'error');
return MigrationInterface::RESULT_FAILED;
}
$return = MigrationInterface::RESULT_COMPLETED;
$source = $this->getSource();
$id_map = $this->migration->getIdMap();
try {
$source->rewind();
}
catch (\Exception $e) {
$this->message->display(
$this->t('Migration failed with source plugin exception: !e', array('!e' => $e->getMessage())), 'error');
return MigrationInterface::RESULT_FAILED;
}
$destination = $this->migration->getDestinationPlugin();
while ($source->valid()) {
$row = $source->current();
if ($this->sourceIdValues = $row->getSourceIdValues()) {
// Wipe old messages, and save any new messages.
$id_map->delete($this->sourceIdValues, TRUE);
$this->saveQueuedMessages();
}
try {
$this->processRow($row);
$save = TRUE;
}
catch (MigrateSkipRowException $e) {
$id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_IGNORED, $this->rollbackAction);
$save = FALSE;
}
if ($save) {
try {
$destination_id_values = $destination->import($row, $id_map->lookupDestinationId($this->sourceIdValues));
if ($destination_id_values) {
// We do not save an idMap entry for config.
if ($destination_id_values !== TRUE) {
$id_map->saveIdMapping($row, $destination_id_values, $this->sourceRowStatus, $this->rollbackAction);
}
$this->successesSinceFeedback++;
$this->totalSuccesses++;
}
else {
$id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction);
if (!$id_map->messageCount()) {
$message = $this->t('New object was not saved, no error provided');
$this->saveMessage($message);
$this->message->display($message);
}
}
}
catch (MigrateException $e) {
$this->migration->getIdMap()->saveIdMapping($row, array(), $e->getStatus(), $this->rollbackAction);
$this->saveMessage($e->getMessage(), $e->getLevel());
$this->message->display($e->getMessage(), 'error');
}
catch (\Exception $e) {
$this->migration->getIdMap()->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction);
$this->handleException($e);
}
}
$this->totalProcessed++;
$this->processedSinceFeedback++;
if ($high_water_property = $this->migration->get('highWaterProperty')) {
$this->migration->saveHighWater($row->getSourceProperty($high_water_property['name']));
}
// Reset row properties.
unset($sourceValues, $destinationValues);
$this->sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
if (($return = $this->checkStatus()) != MigrationInterface::RESULT_COMPLETED) {
break;
}
if ($this->timeOptionExceeded()) {
break;
}
try {
$source->next();
}
catch (\Exception $e) {
$this->message->display(
$this->t('Migration failed with source plugin exception: !e',
array('!e' => $e->getMessage())), 'error');
return MigrationInterface::RESULT_FAILED;
}
}
/**
* @TODO uncomment this
*/
#$this->progressMessage($return);
$this->migration->setMigrationResult($return);
return $return;
}
/**
* {@inheritdoc}
*/
public function processRow(Row $row, array $process = NULL, $value = NULL) {
foreach ($this->migration->getProcessPlugins($process) as $destination => $plugins) {
$multiple = FALSE;
/** @var $plugin \Drupal\migrate\Plugin\MigrateProcessInterface */
foreach ($plugins as $plugin) {
$definition = $plugin->getPluginDefinition();
// Many plugins expect a scalar value but the current value of the
// pipeline might be multiple scalars (this is set by the previous
// plugin) and in this case the current value needs to be iterated
// and each scalar separately transformed.
if ($multiple && !$definition['handle_multiples']) {
$new_value = array();
if (!is_array($value)) {
throw new MigrateException(sprintf('Pipeline failed for destination %s: %s got instead of an array,', $destination, $value));
}
$break = FALSE;
foreach ($value as $scalar_value) {
try {
$new_value[] = $plugin->transform($scalar_value, $this, $row, $destination);
}
catch (MigrateSkipProcessException $e) {
$break = TRUE;
}
}
$value = $new_value;
if ($break) {
break;
}
}
else {
try {
$value = $plugin->transform($value, $this, $row, $destination);
}
catch (MigrateSkipProcessException $e) {
break;
}
$multiple = $multiple || $plugin->multiple();
}
}
// No plugins means do not set.
if ($plugins) {
$row->setDestinationProperty($destination, $value);
}
// Reset the value.
$value = NULL;
}
}
/**
* Fetches the key array for the current source record.
*
* @return array
* The current source IDs.
*/
protected function currentSourceIds() {
return $this->getSource()->getCurrentIds();
}
/**
* Tests whether we've exceeded the designated time limit.
*
* @return bool
* TRUE if the threshold is exceeded, FALSE if not.
*/
protected function timeOptionExceeded() {
// If there is no time limit, then it is not exceeded.
if (!$time_limit = $this->getTimeLimit()) {
return FALSE;
}
// Calculate if the time limit is exceeded.
$time_elapsed = $this->getTimeElapsed();
if ($time_elapsed >= $time_limit) {
return TRUE;
}
else {
return FALSE;
}
}
/**
* {@inheritdoc}
*/
public function getTimeLimit() {
$limit = $this->limit;
if (isset($limit['unit']) && isset($limit['value']) && ($limit['unit'] == 'seconds' || $limit['unit'] == 'second')) {
return $limit['value'];
}
else {
return NULL;
}
}
/**
* {@inheritdoc}
*/
public function saveMessage($message, $level = MigrationInterface::MESSAGE_ERROR) {
$this->migration->getIdMap()->saveMessage($this->sourceIdValues, $message, $level);
}
/**
* {@inheritdoc}
*/
public function queueMessage($message, $level = MigrationInterface::MESSAGE_ERROR) {
$this->queuedMessages[] = array('message' => $message, 'level' => $level);
}
/**
* {@inheritdoc}
*/
public function saveQueuedMessages() {
foreach ($this->queuedMessages as $queued_message) {
$this->saveMessage($queued_message['message'], $queued_message['level']);
}
$this->queuedMessages = array();
}
/**
* Checks for exceptional conditions, and display feedback.
*
* Standard top-of-loop stuff, common between rollback and import.
*/
protected function checkStatus() {
if ($this->memoryExceeded()) {
return MigrationInterface::RESULT_INCOMPLETE;
}
if ($this->maxExecTimeExceeded()) {
return MigrationInterface::RESULT_INCOMPLETE;
}
/*
* @TODO uncomment this
if ($this->getStatus() == MigrationInterface::STATUS_STOPPING) {
return MigrationBase::RESULT_STOPPED;
}
*/
// If feedback is requested, produce a progress message at the proper time
/*
* @TODO uncomment this
if (isset($this->feedback)) {
if (($this->feedback_unit == 'seconds' && time() - $this->lastfeedback >= $this->feedback) ||
($this->feedback_unit == 'items' && $this->processed_since_feedback >= $this->feedback)) {
$this->progressMessage(MigrationInterface::RESULT_INCOMPLETE);
}
}
*/
return MigrationInterface::RESULT_COMPLETED;
}
/**
* Tests whether we've exceeded the desired memory threshold.
*
* If so, output a message.
*
* @return bool
* TRUE if the threshold is exceeded, otherwise FALSE.
*/
protected function memoryExceeded() {
$usage = $this->getMemoryUsage();
$pct_memory = $usage / $this->memoryLimit;
if (!$threshold = $this->memoryThreshold) {
return FALSE;
}
if ($pct_memory > $threshold) {
$this->message->display(
$this->t('Memory usage is !usage (!pct% of limit !limit), reclaiming memory.',
array('!pct' => round($pct_memory*100),
'!usage' => $this->formatSize($usage),
'!limit' => $this->formatSize($this->memoryLimit))),
'warning');
$usage = $this->attemptMemoryReclaim();
$pct_memory = $usage / $this->memoryLimit;
// Use a lower threshold - we don't want to be in a situation where we keep
// coming back here and trimming a tiny amount
if ($pct_memory > (0.90 * $threshold)) {
$this->message->display(
$this->t('Memory usage is now !usage (!pct% of limit !limit), not enough reclaimed, starting new batch',
array('!pct' => round($pct_memory*100),
'!usage' => $this->formatSize($usage),
'!limit' => $this->formatSize($this->memoryLimit))),
'warning');
return TRUE;
}
else {
$this->message->display(
$this->t('Memory usage is now !usage (!pct% of limit !limit), reclaimed enough, continuing',
array('!pct' => round($pct_memory*100),
'!usage' => $this->formatSize($usage),
'!limit' => $this->formatSize($this->memoryLimit))),
'warning');
return FALSE;
}
}
else {
return FALSE;
}
}
/**
* Returns the memory usage so far.
*
* @return int
* The memory usage.
*/
protected function getMemoryUsage() {
return memory_get_usage();
}
/**
* Tries to reclaim memory.
*
* @return int
* The memory usage after reclaim.
*/
protected function attemptMemoryReclaim() {
// First, try resetting Drupal's static storage - this frequently releases
// plenty of memory to continue.
drupal_static_reset();
// @TODO: explore resetting the container.
return memory_get_usage();
}
/**
* Generates a string representation for the given byte count.
*
* @param int $size
* A size in bytes.
*
* @return string
* A translated string representation of the size.
*/
protected function formatSize($size) {
return format_size($size);
}
/**
* Tests whether we're approaching the PHP maximum execution time limit.
*
* @return bool
* TRUE if the threshold is exceeded, FALSE if not.
*/
protected function maxExecTimeExceeded() {
return $this->maxExecTime && (($this->getTimeElapsed() / $this->maxExecTime) > $this->timeThreshold);
}
/**
* Returns the time elapsed.
*
* This allows a test to set a fake elapsed time.
*/
protected function getTimeElapsed() {
return time() - REQUEST_TIME;
}
/**
* Takes an Exception object and both saves and displays it.
*
* Pulls in additional information on the location triggering the exception.
*
* @param \Exception $exception
* Object representing the exception.
* @param bool $save
* (optional) Whether to save the message in the migration's mapping table.
* Set to FALSE in contexts where this doesn't make sense.
*/
protected function handleException(\Exception $exception, $save = TRUE) {
$result = Error::decodeException($exception);
$message = $result['!message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')';
if ($save) {
$this->saveMessage($message);
}
$this->message->display($message, 'error');
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\migrate\MigrateExecutableInterface.
*/
namespace Drupal\migrate;
use Drupal\migrate\Entity\MigrationInterface;
interface MigrateExecutableInterface {
/**
* Performs an import operation - migrate items from source to destination.
*/
public function import();
/**
* Processes a row.
*
* @param \Drupal\migrate\Row $row
* The $row to be processed.
* @param array $process
* (optional) A process pipeline configuration. If not set, the top level
* process configuration in the migration entity is used.
* @param mixed $value
* (optional) Initial value of the pipeline for the first destination.
* Usually setting this is not necessary as $process typically starts with
* a 'get'. This is useful only when the $process contains a single
* destination and needs to access a value outside of the source. See
* \Drupal\migrate\Plugin\migrate\process\Iterator::transformKey for an
* example.
*
* @throws \Drupal\migrate\MigrateException
*/
public function processRow(Row $row, array $process = NULL, $value = NULL);
/**
* Returns the time limit.
*
* @return null|int
* The time limit, NULL if no limit or if the units were not in seconds.
*/
public function getTimeLimit();
/**
* Passes messages through to the map class.
*
* @param string $message
* The message to record.
* @param int $level
* (optional) Message severity (defaults to MESSAGE_ERROR).
*/
public function saveMessage($message, $level = MigrationInterface::MESSAGE_ERROR);
/**
* Queues messages to be later saved through the map class.
*
* @param string $message
* The message to record.
* @param int $level
* (optional) Message severity (defaults to MESSAGE_ERROR).
*/
public function queueMessage($message, $level = MigrationInterface::MESSAGE_ERROR);
/**
* Saves any messages we've queued up to the message table.
*/
public function saveQueuedMessages();
}

View file

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

View file

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

View file

@ -0,0 +1,84 @@
<?php
/**
* @file
* Contains \Drupal\migrate\MigratePassword.
*/
namespace Drupal\migrate;
use Drupal\Core\Password\PasswordInterface;
/**
* Replaces the original 'password' service in order to prefix the MD5 re-hashed
* passwords with the 'U' flag. The new salted hash is recreated on first login
* similarly to the D6->D7 upgrade path.
*/
class MigratePassword implements PasswordInterface {
/**
* The original password service.
*
* @var \Drupal\Core\Password\PasswordInterface
*/
protected $originalPassword;
/**
* Indicates if MD5 password prefixing is enabled.
*/
protected $enabled = FALSE;
/**
* Builds the replacement password service class.
*
* @param \Drupal\Core\Password\PasswordInterface $original_password
* The password object.
*/
public function __construct(PasswordInterface $original_password) {
$this->originalPassword = $original_password;
}
/**
* {@inheritdoc}
*/
public function check($password, $hash) {
return $this->originalPassword->check($password, $hash);
}
/**
* {@inheritdoc}
*/
public function needsRehash($hash) {
return $this->originalPassword->needsRehash($hash);
}
/**
* {@inheritdoc}
*/
public function hash($password) {
$hash = $this->originalPassword->hash($password);
// Allow prefixing only if the service was asked to prefix. Check also if
// the $password pattern is conforming to a MD5 result.
if ($this->enabled && preg_match('/^[0-9a-f]{32}$/', $password)) {
$hash = 'U' . $hash;
}
return $hash;
}
/**
* Enables the MD5 password prefixing.
*/
public function enableMd5Prefixing() {
$this->enabled = TRUE;
}
/**
* Disables the MD5 password prefixing.
*/
public function disableMd5Prefixing() {
$this->enabled = FALSE;
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\migrate\MigrateServiceProvider.
*/
namespace Drupal\migrate;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
/**
* Swaps the original 'password' service in order to handle password hashing for
* user migrations that have passwords hashed to MD5.
*
* @see \Drupal\migrate\MigratePassword
* @see \Drupal\Core\Password\PhpassHashedPassword
*/
class MigrateServiceProvider implements ServiceModifierInterface {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$container->setDefinition('password_original', $container->getDefinition('password'));
$container->setDefinition('password', $container->getDefinition('password_migrate'));
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\migrate\MigrateSkipProcessException.
*/
namespace Drupal\migrate;
/**
* This exception is thrown when the rest of the process should be skipped.
*/
class MigrateSkipProcessException extends \Exception {
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\migrate\MigrateSkipRowException.
*/
namespace Drupal\migrate;
/**
* This exception is thrown when a row should be skipped.
*/
class MigrateSkipRowException extends \Exception {
}

View file

@ -0,0 +1,93 @@
<?php
/**
* @file
* Contains \Drupal\migrate\MigrationStorage.
*/
namespace Drupal\migrate;
use Drupal\Component\Graph\Graph;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
/**
* Storage for migration entities.
*/
class MigrationStorage extends ConfigEntityStorage implements MigrateBuildDependencyInterface {
/**
* {@inheritdoc}
*/
public function buildDependencyMigration(array $migrations, array $dynamic_ids) {
// Migration dependencies defined in the migration storage can be
// optional or required. If an optional dependency does not run, the current
// migration is still OK to go. Both optional and required dependencies
// (if run at all) must run before the current migration.
$dependency_graph = array();
$requirement_graph = array();
$different = FALSE;
foreach ($migrations as $migration) {
/** @var \Drupal\migrate\Entity\MigrationInterface $migration */
$id = $migration->id();
$requirements[$id] = array();
$dependency_graph[$id]['edges'] = array();
$migration_dependencies = $migration->getMigrationDependencies();
if (isset($migration_dependencies['required'])) {
foreach ($migration_dependencies['required'] as $dependency) {
if (!isset($dynamic_ids[$dependency])) {
$this->addDependency($requirement_graph, $id, $dependency, $dynamic_ids);
}
$this->addDependency($dependency_graph, $id, $dependency, $dynamic_ids);
}
}
if (isset($migration_dependencies['optional'])) {
foreach ($migration_dependencies['optional'] as $dependency) {
$different = TRUE;
$this->addDependency($dependency_graph, $id, $dependency, $dynamic_ids);
}
}
}
$graph_object = new Graph($dependency_graph);
$dependency_graph = $graph_object->searchAndSort();
if ($different) {
$graph_object = new Graph($requirement_graph);
$requirement_graph = $graph_object->searchAndSort();
}
else {
$requirement_graph = $dependency_graph;
}
$weights = array();
foreach ($migrations as $migration_id => $migration) {
// Populate a weights array to use with array_multisort later.
$weights[] = $dependency_graph[$migration_id]['weight'];
if (!empty($requirement_graph[$migration_id]['paths'])) {
$migration->set('requirements', $requirement_graph[$migration_id]['paths']);
}
}
array_multisort($weights, SORT_DESC, SORT_NUMERIC, $migrations);
return $migrations;
}
/**
* Add one or more dependencies to a graph.
*
* @param array $graph
* The graph so far.
* @param int $id
* The migration id.
* @param string $dependency
* The dependency string.
* @param array $dynamic_ids
* The dynamic id mapping.
*/
protected function addDependency(array &$graph, $id, $dependency, $dynamic_ids) {
$dependencies = isset($dynamic_ids[$dependency]) ? $dynamic_ids[$dependency] : array($dependency);
if (!isset($graph[$id]['edges'])) {
$graph[$id]['edges'] = array();
}
$graph[$id]['edges'] += array_combine($dependencies, $dependencies);
}
}

View file

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

View file

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

View file

@ -0,0 +1,109 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\MigrateDestinationInterface.
*/
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
/**
* Destinations are responsible for persisting source data into the destination
* Drupal.
*
* @see \Drupal\migrate\Plugin\destination\DestinationBase
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*/
interface MigrateDestinationInterface extends PluginInspectionInterface {
/**
* Get the destination ids.
*
* To support MigrateIdMap maps, derived destination classes should return
* schema field definition(s) corresponding to the primary key of the
* destination being implemented. These are used to construct the destination
* key fields of the map table for a migration using this destination.
*
* @return array
* An array of ids.
*/
public function getIds();
/**
* Returns an array of destination fields.
*
* Derived classes must implement fields(), returning a list of available
* destination fields.
*
* @todo Review the cases where we need the Migration parameter,
* can we avoid that?
*
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* (optional) the migration containing this destination.
*
* @return array
* - Keys: machine names of the fields
* - Values: Human-friendly descriptions of the fields.
*/
public function fields(MigrationInterface $migration = NULL);
/**
* Allows pre-processing of an import.
*
* Derived classes may implement preImport() to do any processing they need
* done before over all source rows.
*/
public function preImport();
/**
* Allows pre-processing of a rollback.
*/
public function preRollback();
/**
* Allows post-processing of an import.
*
* Derived classes may implement postImport(), to do any processing they need
* done after looping over all source rows.
*/
public function postImport();
/**
* Allows post-processing of a rollback.
*/
public function postRollback();
/**
* Import the row.
*
* Derived classes must implement import(), to construct one new object
* (pre-populated) using ID mappings in the Migration).
*
* @param \Drupal\migrate\Row $row
* The row object.
* @param array $old_destination_id_values
* The old destination ids.
*
* @return mixed
* The entity id or an indication of success.
*/
public function import(Row $row, array $old_destination_id_values = array());
/**
* Delete the specified IDs from the target Drupal.
*
* @param array $destination_identifiers
* The destination ids to delete.
*/
public function rollbackMultiple(array $destination_identifiers);
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\MigrateDestinationPluginManager.
*/
namespace Drupal\migrate\Plugin;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\migrate\Entity\MigrationInterface;
/**
* Plugin manager for migrate destination plugins.
*
* @see \Drupal\migrate\Plugin\MigrateDestinationInterface
* @see \Drupal\migrate\Plugin\destination\DestinationBase
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*/
class MigrateDestinationPluginManager extends MigratePluginManager {
/**
* The theme handler
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* An associative array where the keys are the enabled modules and themes.
*
* @var array
*/
protected $providers;
/**
* {@inheritdoc}
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager, $annotation = 'Drupal\migrate\Annotation\MigrateDestination') {
parent::__construct($type, $namespaces, $cache_backend, $module_handler, $annotation);
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*
* A specific createInstance method is necessary to pass the migration on.
*/
public function createInstance($plugin_id, array $configuration = array(), MigrationInterface $migration = NULL) {
if (substr($plugin_id, 0, 7) == 'entity:' && !$this->entityManager->getDefinition(substr($plugin_id, 7), FALSE)) {
$plugin_id = 'null';
}
return parent::createInstance($plugin_id, $configuration, $migration);
}
}

View file

@ -0,0 +1,242 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\MigrateIdMapInterface.
*/
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Row;
/**
* Defines an interface for migrate ID mappings.
*
* Migrate ID mappings maintain a relation between source ID and destination ID
* for audit and rollback purposes.
*/
interface MigrateIdMapInterface extends \Iterator, PluginInspectionInterface {
/**
* Codes reflecting the current status of a map row.
*/
const STATUS_IMPORTED = 0;
const STATUS_NEEDS_UPDATE = 1;
const STATUS_IGNORED = 2;
const STATUS_FAILED = 3;
/**
* Codes reflecting how to handle the destination item on rollback.
*/
const ROLLBACK_DELETE = 0;
const ROLLBACK_PRESERVE = 1;
/**
* Saves a mapping from the source identifiers to the destination identifiers.
*
* Called upon import of one row, we record a mapping from the source ID
* to the destination ID. Also may be called, setting the third parameter to
* NEEDS_UPDATE, to signal an existing record should be re-migrated.
*
* @param \Drupal\migrate\Row $row
* The raw source data. We use the ID map derived from the source object
* to get the source identifier values.
* @param array $destination_id_values
* An array of destination identifier values.
* @param int $status
* Status of the source row in the map.
* @param int $rollback_action
* How to handle the destination object on rollback.
*/
public function saveIdMapping(Row $row, array $destination_id_values, $status = self::STATUS_IMPORTED, $rollback_action = self::ROLLBACK_DELETE);
/**
* Saves a message related to a source record in the migration message table.
*
* @param array $source_id_values
* The source identifier values of the record in error.
* @param string $message
* The message to record.
* @param int $level
* Optional message severity (defaults to MESSAGE_ERROR).
*/
public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR);
/**
* Prepares to run a full update.
*
* Prepares this migration to run as an update - that is, in addition to
* unmigrated content (source records not in the map table) being imported,
* previously-migrated content will also be updated in place by marking all
* previously-imported content as ready to be re-imported.
*/
public function prepareUpdate();
/**
* Returns the number of processed items in the map.
*
* @return int
* The count of records in the map table.
*/
public function processedCount();
/**
* Returns the number of imported items in the map.
*
* @return int
* The number of imported items.
*/
public function importedCount();
/**
* Returns a count of items which are marked as needing update.
*
* @return int
* The number of items which need updating.
*/
public function updateCount();
/**
* Returns the number of items that failed to import.
*
* @return int
* The number of items that errored out.
*/
public function errorCount();
/**
* Returns the number of messages saved.
*
* @return int
* The number of messages.
*/
public function messageCount();
/**
* Deletes the map and message entries for a given source record.
*
* @param array $source_id_values
* The source identifier values of the record to delete.
* @param bool $messages_only
* TRUE to only delete the migrate messages.
*/
public function delete(array $source_id_values, $messages_only = FALSE);
/**
* Deletes the map and message table entries for a given destination row.
*
* @param array $destination_id_values
* The destination identifier values we should do the deletes for.
*/
public function deleteDestination(array $destination_id_values);
/**
* Deletes the map and message entries for a set of given source records.
*
* @param array $source_id_values
* The identifier values of the sources we should do the deletes for. Each
* array member is an array of identifier values for one source row.
*/
public function deleteBulk(array $source_id_values);
/**
* Clears all messages from the map.
*/
public function clearMessages();
/**
* Retrieves a row from the map table based on source identifier values.
*
* @param array $source_id_values
* The source identifier values of the record to retrieve.
*
* @return array
* The raw row data as an associative array.
*/
public function getRowBySource(array $source_id_values);
/**
* Retrieves a row by the destination identifiers.
*
* @param array $destination_id_values
* The destination identifier values of the record to retrieve.
*
* @return array
* The row(s) of data.
*/
public function getRowByDestination(array $destination_id_values);
/**
* Retrieves an array of map rows marked as needing update.
*
* @param int $count
* The maximum number of rows to return.
*
* @return array
* Array of map row objects that need updating.
*/
public function getRowsNeedingUpdate($count);
/**
* Looks up the source identifier.
*
* Given a (possibly multi-field) destination identifier value, return the
* (possibly multi-field) source identifier value mapped to it.
*
* @param array $destination_id_values
* The destination identifier values of the record.
*
* @return array
* The source identifier values of the record, or NULL on failure.
*/
public function lookupSourceID(array $destination_id_values);
/**
* Looks up the destination identifier.
*
* Given a (possibly multi-field) source identifier value, return the
* (possibly multi-field) destination identifier value it is mapped to.
*
* @param array $source_id_values
* The source identifier values of the record.
*
* @return array
* The destination identifier values of the record, or NULL on failure.
*/
public function lookupDestinationId(array $source_id_values);
/**
* Removes any persistent storage used by this map.
*
* For example, remove the map and message tables.
*/
public function destroy();
/**
* Gets the qualified map table.
*
* @todo Remove this as this is SQL only and so doesn't belong to the interface.
*/
public function getQualifiedMapTableName();
/**
* Sets the migrate message.
*
* @param \Drupal\migrate\MigrateMessageInterface $message
* The message to display.
*/
public function setMessage(MigrateMessageInterface $message);
/**
* Sets a specified record to be updated, if it exists.
*
* @param array $source_id_values
* The source identifier values of the record.
*/
public function setUpdate(array $source_id_values);
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\MigratePluginManager.
*/
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\migrate\Entity\MigrationInterface;
/**
* Manages migrate plugins.
*
* @see hook_migrate_info_alter()
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
* @see \Drupal\migrate\Plugin\migrate\process\ProcessPluginBase
* @see plugin_api
*
* @ingroup migration
*/
class MigratePluginManager extends DefaultPluginManager {
/**
* Constructs a MigratePluginManager object.
*
* @param string $type
* The type of the plugin: row, source, process, destination, entity_field,
* id_map.
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
* @param string $annotation
* The annotation class name.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, $annotation = 'Drupal\Component\Annotation\PluginID') {
$plugin_interface = isset($plugin_interface_map[$type]) ? $plugin_interface_map[$type] : NULL;
parent::__construct("Plugin/migrate/$type", $namespaces, $module_handler, $plugin_interface, $annotation);
$this->alterInfo('migrate_' . $type . '_info');
$this->setCacheBackend($cache_backend, 'migrate_plugins_' . $type);
}
/**
* {@inheritdoc}
*
* A specific createInstance method is necessary to pass the migration on.
*/
public function createInstance($plugin_id, array $configuration = array(), MigrationInterface $migration = NULL) {
$plugin_definition = $this->getDefinition($plugin_id);
$plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition);
// If the plugin provides a factory method, pass the container to it.
if (is_subclass_of($plugin_class, 'Drupal\Core\Plugin\ContainerFactoryPluginInterface')) {
$plugin = $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition, $migration);
}
else {
$plugin = new $plugin_class($configuration, $plugin_id, $plugin_definition, $migration);
}
return $plugin;
}
/**
* Helper for the plugin type to interface map.
*
* @return array
* An array map from plugin type to interface.
*/
protected function getPluginInterfaceMap() {
return [
'destination' => 'Drupal\migrate\Plugin\MigrateDestinationInterface',
'process' => 'Drupal\migrate\Plugin\MigrateProcessInterface',
'source' => 'Drupal\migrate\Plugin\MigrateSourceInterface',
'id_map' => 'Drupal\migrate\Plugin\MigrateIdMapInterface',
'entity_field' => 'Drupal\migrate\Plugin\MigrateEntityDestinationFieldInterface',
];
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\MigrateProcessInterface.
*/
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* An interface for migrate process plugins.
*
* A process plugin can use any number of methods instead of (but not in
* addition to) transform with the same arguments and then the plugin
* configuration needs to provide the name of the method to be called via the
* "method" key. See \Drupal\migrate\Plugin\migrate\process\SkipOnEmpty and
* migrate.migration.d6_field_instance_widget_settings.yml for examples.
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\ProcessPluginBase
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see plugin_api
*
* @ingroup migration
*/
interface MigrateProcessInterface extends PluginInspectionInterface {
/**
* Performs the associated process.
*
* @param mixed $value
* The value to be transformed.
* @param \Drupal\migrate\MigrateExecutableInterface $migrate_executable
* The migration in which this process is being executed.
* @param \Drupal\migrate\Row $row
* The row from the source to process. Normally, just transforming the
* value is adequate but very rarely you might need to change two columns
* at the same time or something like that.
* @param string $destination_property
* The destination property currently worked on. This is only used
* together with the $row above.
*
* @return string|array
* The newly transformed value.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property);
/**
* Indicates whether the returned value requires multiple handling.
*
* @return bool
* TRUE when the returned value contains a list of values to be processed.
* For example, when the 'source' property is a string and the value found
* is an array.
*/
public function multiple();
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\MigrateSourceInterface.
*/
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\Row;
/**
* Defines an interface for migrate sources.
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @see plugin_api
*
* @ingroup migration
*/
interface MigrateSourceInterface extends \Countable, \Iterator, PluginInspectionInterface {
/**
* Returns available fields on the source.
*
* @return array
* Available fields in the source, keys are the field machine names as used
* in field mappings, values are descriptions.
*/
public function fields();
/**
* Returns the iterator that will yield the row arrays to be processed.
*
* @return \Iterator
* The iterator object.
*
* @throws \Exception
* Cannot obtain a valid iterator.
*/
public function getIterator();
/**
* Add additional data to the row.
*
* @param \Drupal\Migrate\Row $row
* The row object.
*
* @return bool
* FALSE if this row needs to be skipped.
*/
public function prepareRow(Row $row);
public function __toString();
/**
* Get the source ids.
*
* @return array
* The source ids.
*/
public function getIds();
}

View file

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

View file

@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\SourceEntityInterface.
*/
namespace Drupal\migrate\Plugin;
/**
* Interface for sources providing an entity.
*/
interface SourceEntityInterface {
/**
* Whether this migration has a bundle migration.
*
* @return bool
* TRUE when the bundle_migration key is required.
*/
public function bundleMigrationRequired();
/**
* The entity type id (user, node etc).
*
* This function is used when bundleMigrationRequired() is FALSE.
*
* @return string
* The entity type id.
*/
public function entityTypeId();
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\Book.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityInterface;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "book",
* provider = "book"
* )
*/
class Book extends EntityContentBase {
/**
* {@inheritdoc}
*/
protected static function getEntityTypeId($plugin_id) {
return 'node';
}
/**
* {@inheritdoc}
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
$entity->book = $row->getDestinationProperty('book');
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\ComponentEntityDisplayBase.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
abstract class ComponentEntityDisplayBase extends DestinationBase {
const MODE_NAME = '';
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$values = array();
// array_intersect_key() won't work because the order is important because
// this is also the return value.
foreach (array_keys($this->getIds()) as $id) {
$values[$id] = $row->getDestinationProperty($id);
}
$entity = $this->getEntity($values['entity_type'], $values['bundle'], $values[static::MODE_NAME]);
if (!$row->getDestinationProperty('hidden')) {
$entity->setComponent($values['field_name'], $row->getDestinationProperty('options') ?: array());
}
else {
$entity->removeComponent($values['field_name']);
}
$entity->save();
return array_values($values);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['entity_type']['type'] = 'string';
$ids['bundle']['type'] = 'string';
$ids[static::MODE_NAME]['type'] = 'string';
$ids['field_name']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
// This is intentionally left empty.
}
/**
* Get the entity.
*
* @param string $entity_type
* The entity type to retrieve.
* @param string $bundle
* The entity bundle.
* @param string $mode
* The display mode.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface
* The entity display object.
*/
protected abstract function getEntity($entity_type, $bundle, $mode);
}

View file

@ -0,0 +1,121 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\Config.
*
* Provides Configuration Management destination plugin.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Config\Config as ConfigObject;
/**
* Persist data to the config system.
*
* When a property is NULL, the default is used unless the configuration option
* 'store null' is set to TRUE.
*
* @MigrateDestination(
* id = "config"
* )
*/
class Config extends DestinationBase implements ContainerFactoryPluginInterface, DependentPluginInterface {
use DependencyTrait;
/**
* The config object.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* Constructs a Config destination object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The migration entity.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->config = $config_factory->getEditable($configuration['config_name']);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
foreach ($row->getRawDestination() as $key => $value) {
if (isset($value) || !empty($this->configuration['store null'])) {
$this->config->set(str_replace(Row::PROPERTY_SEPARATOR, '.', $key), $value);
}
}
$this->config->save();
return TRUE;
}
/**
* Throw an exception because config can not be rolled back.
*
* @param array $destination_keys
* The array of destination ids to roll back.
*
* @throws \Drupal\migrate\MigrateException
*/
public function rollbackMultiple(array $destination_keys) {
throw new MigrateException('Configuration can not be rolled back');
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
// @todo Dynamically fetch fields using Config Schema API.
}
/**
* {@inheritdoc}
*/
public function getIds() {
return array();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$provider = explode('.', $this->config->getName(), 2)[0];
$this->addDependency('module', $provider);
return $this->dependencies;
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\DestinationBase.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\RequirementsInterface;
/**
* Base class for migrate destination classes.
*
* @see \Drupal\migrate\Plugin\MigrateDestinationInterface
* @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager
* @see \Drupal\migrate\Annotation\MigrateDestination
* @see plugin_api
*
* @ingroup migration
*/
abstract class DestinationBase extends PluginBase implements MigrateDestinationInterface, RequirementsInterface {
/**
* The migration.
*
* @var \Drupal\migrate\Entity\MigrationInterface
*/
protected $migration;
/**
* Constructs an entity destination plugin.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param MigrationInterface $migration
* The migration.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
if (empty($this->pluginDefinition['requirements_met'])) {
throw new RequirementsException();
}
}
/**
* Modify the Row before it is imported.
*/
public function preImport() {
// By default we do nothing.
}
/**
* Modify the Row before it is rolled back.
*/
public function preRollback() {
// By default we do nothing.
}
/**
* {@inheritdoc}
*/
public function postImport() {
// By default we do nothing.
}
/**
* {@inheritdoc}
*/
public function postRollback() {
// By default we do nothing.
}
/**
* {@inheritdoc}
*/
public function rollbackMultiple(array $destination_identifiers) {
// By default we do nothing.
}
/**
* {@inheritdoc}
*/
public function getCreated() {
// TODO: Implement getCreated() method.
}
/**
* {@inheritdoc}
*/
public function getUpdated() {
// TODO: Implement getUpdated() method.
}
/**
* {@inheritdoc}
*/
public function resetStats() {
// TODO: Implement resetStats() method.
}
}

View file

@ -0,0 +1,182 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\Entity.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @MigrateDestination(
* id = "entity",
* deriver = "Drupal\migrate\Plugin\Derivative\MigrateEntity"
* )
*/
abstract class Entity extends DestinationBase implements ContainerFactoryPluginInterface, DependentPluginInterface {
use DependencyTrait;
/**
* The entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* The list of the bundles of this entity type.
*
* @var array
*/
protected $bundles;
/**
* Construct a new entity.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param MigrationInterface $migration
* The migration.
* @param EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->storage = $storage;
$this->bundles = $bundles;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$entity_type_id = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity.manager')->getStorage($entity_type_id),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type_id))
);
}
/**
* Finds the entity type from configuration or plugin id.
*
* @param string $plugin_id
* The plugin id.
*
* @return string
* The entity type.
*/
protected static function getEntityTypeId($plugin_id) {
// Remove "entity:"
return substr($plugin_id, 7);
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
// TODO: Implement fields() method.
}
/**
* Creates or loads an entity.
*
* @param \Drupal\migrate\Row $row
* The row object.
* @param array $old_destination_id_values
* The old destination ids.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity we're importing into.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
$entity_id = $old_destination_id_values ? reset($old_destination_id_values) : $this->getEntityId($row);
if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
$this->updateEntity($entity, $row);
}
else {
$values = $row->getDestination();
// Stubs might not have the bundle specified.
if ($row->isStub()) {
$values = $this->processStubValues($values);
}
$entity = $this->storage->create($values);
$entity->enforceIsNew();
}
return $entity;
}
/**
* Get the entity id of the row.
*
* @param \Drupal\migrate\Row $row
* The row of data.
* @return string
* The entity id for the row we're importing.
*/
protected function getEntityId(Row $row) {
return $row->getDestinationProperty($this->getKey('id'));
}
/**
* Process the stub values.
*
* @param array $values
* An array of destination values.
*
* @return array
* The processed stub values.
*/
protected function processStubValues(array $values) {
$values = array_intersect_key($values, $this->getIds());
$bundle_key = $this->getKey('bundle');
if ($bundle_key && !isset($values[$bundle_key])) {
$values[$bundle_key] = reset($this->bundles);
}
return $values;
}
/**
* Returns a specific entity key.
*
* @param string $key
* The name of the entity key to return.
*
* @return string|bool
* The entity key, or FALSE if it does not exist.
*
* @see \Drupal\Core\Entity\EntityTypeInterface::getKeys()
*/
protected function getKey($key) {
return $this->storage->getEntityType()->getKey($key);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$this->addDependency('module', $this->storage->getEntityType()->getProvider());
return $this->dependencies;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityBaseFieldOverride.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity:base_field_override"
* )
*/
class EntityBaseFieldOverride extends EntityConfigBase {
/**
* {@inheritdoc}
*/
protected function getEntityId(Row $row) {
$entity_type = $row->getDestinationProperty('entity_type');
$bundle = $row->getDestinationProperty('bundle');
$field_name = $row->getDestinationProperty('field_name');
return "$entity_type.$bundle.$field_name";
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityComment.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\MigratePluginManager;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @MigrateDestination(
* id = "entity:comment"
* )
*/
class EntityComment extends EntityContentBase {
/**
* The state storage object.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Builds an comment entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param MigrationInterface $migration
* The migration.
* @param EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager
* The migrate plugin manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\State\StateInterface $state
* The state storage object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, StateInterface $state) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager);
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$entity_type = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity.manager')->getStorage($entity_type),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
$container->get('entity.manager'),
$container->get('state')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
if ($row->isStub() && ($state = $this->state->get('comment.maintain_entity_statistics', 0))) {
$this->state->set('comment.maintain_entity_statistics', 0);
}
$return = parent::import($row, $old_destination_id_values);
if ($row->isStub() && $state) {
$this->state->set('comment.maintain_entity_statistics', $state);
}
return $return;
}
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityCommentType.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity:comment_type"
* )
*/
class EntityCommentType extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$entity_ids = parent::import($row, $old_destination_id_values);
\Drupal::service('comment.manager')->addBodyField(reset($entity_ids));
return $entity_ids;
}
}

View file

@ -0,0 +1,123 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityConfigBase.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
/**
* Class for importing configuration entities.
*
* This class serves as the import class for most configuration entities.
* It can be necessary to provide a specific entity class if the configuration
* entity has a compound id (see EntityFieldEntity) or it has specific setter
* methods (see EntityDateFormat). When implementing an entity destination for
* the latter case, make sure to add a test not only for importing but also
* for re-importing (if that is supported).
*/
class EntityConfigBase extends Entity {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
if ($row->isStub()) {
throw new MigrateException('Config entities can not be stubbed.');
}
$ids = $this->getIds();
$id_key = $this->getKey('id');
if (count($ids) > 1) {
// Ids is keyed by the key name so grab the keys.
$id_keys = array_keys($ids);
if (!$row->getDestinationProperty($id_key)) {
// Set the id into the destination in for form "val1.val2.val3".
$row->setDestinationProperty($id_key, $this->generateId($row, $id_keys));
}
}
$entity = $this->getEntity($row, $old_destination_id_values);
$entity->save();
if (count($ids) > 1) {
// This can only be a config entity, content entities have their id key
// and that's it.
$return = array();
foreach ($id_keys as $id_key) {
$return[] = $entity->get($id_key);
}
return $return;
}
return array($entity->id());
}
/**
* {@inheritdoc}
*/
public function getIds() {
$id_key = $this->getKey('id');
$ids[$id_key]['type'] = 'string';
return $ids;
}
/**
* Updates an entity with the contents of a row.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to update.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
foreach ($row->getRawDestination() as $property => $value) {
$this->updateEntityProperty($entity, explode(Row::PROPERTY_SEPARATOR, $property), $value);
}
}
/**
* Updates a (possible nested) entity property with a value.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The config entity.
* @param array $parents
* The array of parents.
* @param string|object $value
* The value to update to.
*/
protected function updateEntityProperty(EntityInterface $entity, array $parents, $value) {
$top_key = array_shift($parents);
$entity_value = $entity->get($top_key);
if (is_array($entity_value)) {
NestedArray::setValue($entity_value, $parents, $value);
}
else {
$entity_value = $value;
}
$entity->set($top_key, $entity_value);
}
/**
* Generate an entity id.
*
* @param \Drupal\migrate\Row $row
* The current row.
* @param array $ids
* The destination ids.
*
* @return string
* The generated entity id.
*/
protected function generateId(Row $row, array $ids) {
$id_values = array();
foreach ($ids as $id) {
$id_values[] = $row->getDestinationProperty($id);
}
return implode('.', $id_values);
}
}

View file

@ -0,0 +1,122 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityContentBase.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* The destination class for all content entities lacking a specific class.
*/
class EntityContentBase extends Entity {
/**
* Entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a content entity.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The migration entity.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager
* The plugin manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles);
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$entity_type = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity.manager')->getStorage($entity_type),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
$container->get('entity.manager')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$entity = $this->getEntity($row, $old_destination_id_values);
return $this->save($entity, $old_destination_id_values);
}
/**
* Save the entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The content entity.
* @param array $old_destination_id_values
* An array of destination id values.
*
* @return array
* An array containing the entity id.
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = array()) {
$entity->save();
return array($entity->id());
}
/**
* {@inheritdoc}
*/
public function getIds() {
$id_key = $this->getKey('id');
$ids[$id_key]['type'] = 'integer';
return $ids;
}
/**
* Update an entity with the new values from row.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to update.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
foreach ($row->getDestination() as $field_name => $values) {
$field = $entity->$field_name;
if ($field instanceof TypedDataInterface) {
$field->setValue($values);
}
}
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityDateFormat.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityInterface;
/**
* @MigrateDestination(
* id = "entity:date_format"
* )
*/
class EntityDateFormat extends EntityConfigBase {
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Datetime\DateFormatInterface $entity
* The date entity.
*/
protected function updateEntityProperty(EntityInterface $entity, array $parents, $value) {
if ($parents[0] == 'pattern') {
$entity->setPattern($value);
}
else {
parent::updateEntityProperty($entity, $parents, $value);
}
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityFieldInstance.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity:field_config"
* )
*/
class EntityFieldInstance extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['entity_type']['type'] = 'string';
$ids['bundle']['type'] = 'string';
$ids['field_name']['type'] = 'string';
return $ids;
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityFieldStorageConfig.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
/**
* @MigrateDestination(
* id = "entity:field_storage_config"
* )
*/
class EntityFieldStorageConfig extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['entity_type']['type'] = 'string';
$ids['field_name']['type'] = 'string';
return $ids;
}
}

View file

@ -0,0 +1,245 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityFile.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateException;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Every migration that uses this destination must have an optional
* dependency on the d6_file migration to ensure it runs first.
*
* @MigrateDestination(
* id = "entity:file"
* )
*/
class EntityFile extends EntityContentBase {
/**
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system) {
$configuration += array(
'source_base_path' => '',
'source_path_property' => 'filepath',
'destination_path_property' => 'uri',
'move' => FALSE,
'urlencode' => FALSE,
);
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager);
$this->streamWrapperManager = $stream_wrappers;
$this->fileSystem = $file_system;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$entity_type = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity.manager')->getStorage($entity_type),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
$container->get('entity.manager'),
$container->get('stream_wrapper_manager'),
$container->get('file_system')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$file = $row->getSourceProperty($this->configuration['source_path_property']);
$destination = $row->getDestinationProperty($this->configuration['destination_path_property']);
$source = $this->configuration['source_base_path'] . $file;
// Ensure the source file exists, if it's a local URI or path.
if ($this->isLocalUri($source) && !file_exists($source)) {
throw new MigrateException(SafeMarkup::format('File @source does not exist.', ['@source' => $source]));
}
// If the start and end file is exactly the same, there is nothing to do.
if ($this->isLocationUnchanged($source, $destination)) {
return parent::import($row, $old_destination_id_values);
}
$replace = $this->getOverwriteMode($row);
$success = $this->writeFile($source, $destination, $replace);
if (!$success) {
$dir = $this->getDirectory($destination);
if (file_prepare_directory($dir, FILE_CREATE_DIRECTORY)) {
$success = $this->writeFile($source, $destination, $replace);
}
else {
throw new MigrateException(SafeMarkup::format('Could not create directory @dir', ['@dir' => $dir]));
}
}
if ($success) {
return parent::import($row, $old_destination_id_values);
}
else {
throw new MigrateException(SafeMarkup::format('File %source could not be copied to %destination.', ['%source' => $source, '%destination' => $destination]));
}
}
/**
* Tries to move or copy a file.
*
* @param string $source
* The source path or URI.
* @param string $destination
* The destination path or URI.
* @param integer $replace
* FILE_EXISTS_REPLACE (default) or FILE_EXISTS_RENAME.
*
* @return boolean
* TRUE on success, FALSE on failure.
*/
protected function writeFile($source, $destination, $replace = FILE_EXISTS_REPLACE) {
if ($this->configuration['move']) {
return (boolean) file_unmanaged_move($source, $destination, $replace);
}
else {
$destination = file_destination($destination, $replace);
$source = $this->urlencode($source);
return @copy($source, $destination);
}
}
/**
* Determines how to handle file conflicts.
*
* @param \Drupal\migrate\Row $row
*
* @return integer
* Either FILE_EXISTS_REPLACE (default) or FILE_EXISTS_RENAME, depending
* on the current configuration.
*/
protected function getOverwriteMode(Row $row) {
if (!empty($this->configuration['rename'])) {
$entity_id = $row->getDestinationProperty($this->getKey('id'));
if ($entity_id && ($entity = $this->storage->load($entity_id))) {
return FILE_EXISTS_RENAME;
}
}
return FILE_EXISTS_REPLACE;
}
/**
* Returns the directory component of a URI or path.
*
* For URIs like public://foo.txt, the full physical path of public://
* will be returned, since a scheme by itself will trip up certain file
* API functions (such as file_prepare_directory()).
*
* @param string $uri
* The URI or path.
*
* @return boolean|string
* The directory component of the path or URI, or FALSE if it could not
* be determined.
*/
protected function getDirectory($uri) {
$dir = $this->fileSystem->dirname($uri);
if (substr($dir, -3) == '://') {
return $this->fileSystem->realpath($dir);
}
else {
return $dir;
}
}
/**
* Returns if the source and destination URIs represent identical paths.
* If either URI is a remote stream, will return FALSE.
*
* @param string $source
* The source URI.
* @param string $destination
* The destination URI.
*
* @return boolean
* TRUE if the source and destination URIs refer to the same physical path,
* otherwise FALSE.
*/
protected function isLocationUnchanged($source, $destination) {
if ($this->isLocalUri($source) && $this->isLocalUri($destination)) {
return $this->fileSystem->realpath($source) === $this->fileSystem->realpath($destination);
}
else {
return FALSE;
}
}
/**
* Returns if the given URI or path is considered local.
*
* A URI or path is considered local if it either has no scheme component,
* or the scheme is implemented by a stream wrapper which extends
* \Drupal\Core\StreamWrapper\LocalStream.
*
* @param string $uri
* The URI or path to test.
*
* @return boolean
*/
protected function isLocalUri($uri) {
$scheme = $this->fileSystem->uriScheme($uri);
return $scheme === FALSE || $this->streamWrapperManager->getViaScheme($scheme) instanceof LocalStream;
}
/**
* Urlencode all the components of a remote filename.
*
* @param string $filename
* The filename of the file to be urlencoded.
*
* @return string
* The urlencoded filename.
*/
protected function urlencode($filename) {
// Only apply to a full URL
if ($this->configuration['urlencode'] && strpos($filename, '://')) {
$components = explode('/', $filename);
foreach ($components as $key => $component) {
$components[$key] = rawurlencode($component);
}
$filename = implode('/', $components);
// Actually, we don't want certain characters encoded
$filename = str_replace('%3A', ':', $filename);
$filename = str_replace('%3F', '?', $filename);
$filename = str_replace('%26', '&', $filename);
}
return $filename;
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityNodeType.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity:node_type"
* )
*/
class EntityNodeType extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$entity_ids = parent::import($row, $old_destination_id_values);
if ($row->getDestinationProperty('create_body')) {
$node_type = $this->storage->load(reset($entity_ids));
node_add_body_field($node_type, $row->getDestinationProperty('create_body_label'));
}
return $entity_ids;
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityRevision.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity_revision",
* deriver = "Drupal\migrate\Plugin\Derivative\MigrateEntityRevision"
* )
*/
class EntityRevision extends EntityContentBase {
/**
* {@inheritdoc}
*/
protected static function getEntityTypeId($plugin_id) {
// Remove entity_revision:
return substr($plugin_id, 16);
}
/**
* Get the entity.
*
* @param \Drupal\migrate\Row $row
* The row object.
*
* @return \Drupal\Core\Entity\EntityInterface|false
* The entity or false if it can not be created.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
$revision_id = $old_destination_id_values ? reset($old_destination_id_values) : $row->getDestinationProperty($this->getKey('revision'));
if (!empty($revision_id) && ($entity = $this->storage->loadRevision($revision_id))) {
$entity->setNewRevision(FALSE);
}
else {
$entity_id = $row->getDestinationProperty($this->getKey('id'));
$entity = $this->storage->load($entity_id);
$entity->enforceIsNew(FALSE);
$entity->setNewRevision(TRUE);
}
$this->updateEntity($entity, $row);
$entity->isDefaultRevision(FALSE);
return $entity;
}
/**
* {@inheritdoc}
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = array()) {
$entity->save();
return array($entity->getRevisionId());
}
/**
* {@inheritdoc}
*/
public function getIds() {
if ($key = $this->getKey('revision')) {
$ids[$key]['type'] = 'integer';
return $ids;
}
throw new MigrateException('This entity type does not support revisions.');
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntitySearchPage.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityInterface;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity:search_page"
* )
*/
class EntitySearchPage extends EntityConfigBase {
/**
* Updates the entity with the contents of a row.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The search page entity.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
$entity->setPlugin($row->getDestinationProperty('plugin'));
$entity->getPlugin()->setConfiguration($row->getDestinationProperty('configuration'));
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityTaxonomyTerm.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity:taxonomy_term"
* )
*/
class EntityTaxonomyTerm extends EntityContentBase {
/**
* {@inheritdoc}
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
if ($row->isStub()) {
$row->setDestinationProperty('name', $this->t('Stub name for source tid:') . $row->getSourceProperty('tid'));
}
return parent::getEntity($row, $old_destination_id_values);
}
}

View file

@ -0,0 +1,101 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityUser.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Password\PasswordInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigratePassword;
use Drupal\migrate\Plugin\MigratePluginManager;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @MigrateDestination(
* id = "entity:user"
* )
*/
class EntityUser extends EntityContentBase {
/**
* The password service class.
*
* @var \Drupal\Core\Password\PasswordInterface
*/
protected $password;
/**
* Builds an user entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param MigrationInterface $migration
* The migration.
* @param EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager
* The migrate plugin manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Password\PasswordInterface $password
* The password service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, PasswordInterface $password) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager);
if (isset($configuration['md5_passwords'])) {
$this->password = $password;
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$entity_type = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity.manager')->getStorage($entity_type),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
$container->get('entity.manager'),
$container->get('password')
);
}
/**
* {@inheritdoc}
* @throws \Drupal\migrate\MigrateException
*/
public function import(Row $row, array $old_destination_id_values = array()) {
if ($this->password) {
if ($this->password instanceof MigratePassword) {
$this->password->enableMd5Prefixing();
}
else {
throw new MigrateException('Password service has been altered by another module, aborting.');
}
}
$ids = parent::import($row, $old_destination_id_values);
if ($this->password) {
$this->password->disableMd5Prefixing();
}
return $ids;
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\EntityViewMode.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
/**
* @MigrateDestination(
* id = "entity:entity_view_mode"
* )
*/
class EntityViewMode extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['targetEntityType']['type'] = 'string';
$ids['mode']['type'] = 'string';
return $ids;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\Null.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "null",
* requirements_met = false
* )
*/
class Null extends DestinationBase {
/**
* {@inheritdoc}
*/
public function getIds() {
return array();
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
return array();
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
}
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\PerComponentEntityDisplay.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
/**
* This class imports one component of an entity display.
*
* @MigrateDestination(
* id = "component_entity_display"
* )
*/
class PerComponentEntityDisplay extends ComponentEntityDisplayBase {
const MODE_NAME = 'view_mode';
/**
* {@inheritdoc}
*/
protected function getEntity($entity_type, $bundle, $view_mode) {
return entity_get_display($entity_type, $bundle, $view_mode);
}
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\PerComponentEntityFormDisplay.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
/**
* This class imports one component of an entity form display.
*
* @MigrateDestination(
* id = "component_entity_form_display"
* )
*/
class PerComponentEntityFormDisplay extends ComponentEntityDisplayBase {
const MODE_NAME = 'form_mode';
/**
* {@inheritdoc}
*/
protected function getEntity($entity_type, $bundle, $form_mode) {
return entity_get_form_display($entity_type, $bundle, $form_mode);
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\UrlAlias.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Path\AliasStorage;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* @MigrateDestination(
* id = "url_alias"
* )
*/
class UrlAlias extends DestinationBase implements ContainerFactoryPluginInterface {
/**
* The alias storage service.
*
* @var \Drupal\Core\Path\AliasStorage $aliasStorage
*/
protected $aliasStorage;
/**
* Constructs an entity destination plugin.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Path\AliasStorage $alias_storage
* The alias storage service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, AliasStorage $alias_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->aliasStorage = $alias_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('path.alias_storage')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$path = $this->aliasStorage->save(
$row->getDestinationProperty('source'),
$row->getDestinationProperty('alias'),
$row->getDestinationProperty('langcode'),
$old_destination_id_values ? $old_destination_id_values[0] : NULL
);
return array($path['pid']);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['pid']['type'] = 'integer';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
return [
'pid' => 'The path id',
'source' => 'The source path.',
'alias' => 'The url alias.',
'langcode' => 'The language code for the url.',
];
}
}

View file

@ -0,0 +1,94 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\destination\UserData.
*/
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\user\UserData as UserDataStorage;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* @MigrateDestination(
* id = "user_data"
* )
*/
class UserData extends DestinationBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\user\UserData
*/
protected $userData;
/**
* Builds an user data entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The migration.
* @param \Drupal\user\UserData $user_data
* The user data service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, UserDataStorage $user_data) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->userData = $user_data;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('user.data')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$uid = $row->getDestinationProperty('uid');
$module = $row->getDestinationProperty('module');
$key = $row->getDestinationProperty('key');
$this->userData->set($module, $uid, $key, $row->getDestinationProperty('settings'));
return [$uid, $module, $key];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['uid']['type'] = 'integer';
$ids['module']['type'] = 'string';
$ids['key']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
return [
'uid' => 'The user id.',
'module' => 'The module name responsible for the settings.',
'key' => 'The setting key to save under.',
'settings' => 'The settings to save.',
];
}
}

View file

@ -0,0 +1,793 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\id_map\Sql.
*/
namespace Drupal\migrate\Plugin\migrate\id_map;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\Connection;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
/**
* Defines the sql based ID map implementation.
*
* It creates one map and one message table per migration entity to store the
* relevant information.
*
* @PluginID("sql")
*/
class Sql extends PluginBase implements MigrateIdMapInterface {
/**
* The migration map table name.
*
* @var string
*/
protected $mapTableName;
/**
* The message table name.
*
* @var string
*/
protected $messageTableName;
/**
* The migrate message.
*
* @var \Drupal\migrate\MigrateMessageInterface
*/
protected $message;
/**
* The database connection for the map/message tables on the destination.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* @var \Drupal\Core\Database\Query\SelectInterface
*/
protected $query;
/**
* The migration being done.
*
* @var \Drupal\migrate\Entity\MigrationInterface
*/
protected $migration;
/**
* The source ID fields.
*
* @var array
*/
protected $sourceIdFields;
/**
* The destination ID fields.
*
* @var array
*/
protected $destinationIdFields;
/**
* Whether the plugin is already initialized.
*
* @var bool
*/
protected $initialized;
/**
* The result.
*
* @var null
*/
protected $result = NULL;
/**
* The source identifiers.
*
* @var array
*/
protected $sourceIds = array();
/**
* The destination identifiers.
*
* @var array
*/
protected $destinationIds = array();
/**
* The current row.
*
* @var null
*/
protected $currentRow = NULL;
/**
* The current key.
*
* @var array
*/
protected $currentKey = array();
/**
* Constructs an SQL object.
*
* Sets up the tables and builds the maps,
*
* @param array $configuration
* The configuration.
* @param string $plugin_id
* The plugin ID for the migration process to do.
* @param mixed $plugin_definition
* The configuration for the plugin.
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The migration to do.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
}
/**
* The source ID fields.
*
* @return array
* The source ID fields.
*/
protected function sourceIdFields() {
if (!isset($this->sourceIdFields)) {
// Build the source and destination identifier maps.
$this->sourceIdFields = array();
$count = 1;
foreach ($this->migration->getSourcePlugin()->getIds() as $field => $schema) {
$this->sourceIdFields[$field] = 'sourceid' . $count++;
}
}
return $this->sourceIdFields;
}
/**
* The destination ID fields.
*
* @return array
* The destination ID fields.
*/
protected function destinationIdFields() {
if (!isset($this->destinationIdFields)) {
$this->destinationIdFields = array();
$count = 1;
foreach ($this->migration->getDestinationPlugin()->getIds() as $field => $schema) {
$this->destinationIdFields[$field] = 'destid' . $count++;
}
}
return $this->destinationIdFields;
}
/**
* The name of the database map table.
*
* @return string
* The map table name.
*/
public function mapTableName() {
$this->init();
return $this->mapTableName;
}
/**
* The name of the database message table.
*
* @return string
* The message table name.
*/
public function messageTableName() {
$this->init();
return $this->messageTableName;
}
/**
* Get the fully qualified map table name.
*
* @return string
* The fully qualified map table name.
*/
public function getQualifiedMapTableName() {
return $this->getDatabase()->getFullQualifiedTableName($this->mapTableName);
}
/**
* Gets the database connection.
*
* @return \Drupal\Core\Database\Connection
* The database connection object.
*/
public function getDatabase() {
if (!isset($this->database)) {
$this->database = \Drupal::database();
}
$this->init();
return $this->database;
}
/**
* Initialize the plugin.
*/
protected function init() {
if (!$this->initialized) {
$this->initialized = TRUE;
// Default generated table names, limited to 63 characters.
$machine_name = str_replace(':', '__', $this->migration->id());
$prefix_length = strlen($this->getDatabase()->tablePrefix());
$this->mapTableName = 'migrate_map_' . Unicode::strtolower($machine_name);
$this->mapTableName = Unicode::substr($this->mapTableName, 0, 63 - $prefix_length);
$this->messageTableName = 'migrate_message_' . Unicode::strtolower($machine_name);
$this->messageTableName = Unicode::substr($this->messageTableName, 0, 63 - $prefix_length);
$this->ensureTables();
}
}
/**
* {@inheritdoc}
*/
public function setMessage(MigrateMessageInterface $message) {
$this->message = $message;
}
/**
* Create the map and message tables if they don't already exist.
*/
protected function ensureTables() {
if (!$this->getDatabase()->schema()->tableExists($this->mapTableName)) {
// Generate appropriate schema info for the map and message tables,
// and map from the source field names to the map/msg field names.
$count = 1;
$source_id_schema = array();
$pks = array();
foreach ($this->migration->getSourcePlugin()->getIds() as $id_definition) {
$mapkey = 'sourceid' . $count++;
$source_id_schema[$mapkey] = $this->getFieldSchema($id_definition);
// With InnoDB, utf8mb4-based primary keys can't be over 191 characters.
// Use ASCII-based primary keys instead.
if (isset($source_id_schema[$mapkey]['type']) && $source_id_schema[$mapkey]['type'] == 'varchar') {
$source_id_schema[$mapkey]['type'] = 'varchar_ascii';
}
$pks[] = $mapkey;
}
$fields = $source_id_schema;
// Add destination identifiers to map table.
// TODO: How do we discover the destination schema?
$count = 1;
foreach ($this->migration->getDestinationPlugin()->getIds() as $id_definition) {
// Allow dest identifier fields to be NULL (for IGNORED/FAILED
// cases).
$mapkey = 'destid' . $count++;
$fields[$mapkey] = $this->getFieldSchema($id_definition);
$fields[$mapkey]['not null'] = FALSE;
}
$fields['source_row_status'] = array(
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::STATUS_IMPORTED,
'description' => 'Indicates current status of the source row',
);
$fields['rollback_action'] = array(
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::ROLLBACK_DELETE,
'description' => 'Flag indicating what to do for this item on rollback',
);
$fields['last_imported'] = array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'UNIX timestamp of the last time this row was imported',
);
$fields['hash'] = array(
'type' => 'varchar',
'length' => '64',
'not null' => FALSE,
'description' => 'Hash of source row data, for detecting changes',
);
$schema = array(
'description' => 'Mappings from source identifier value(s) to destination identifier value(s).',
'fields' => $fields,
);
if ($pks) {
$schema['primary key'] = $pks;
}
$this->getDatabase()->schema()->createTable($this->mapTableName, $schema);
// Now do the message table.
if (!$this->getDatabase()->schema()->tableExists($this->messageTableName())) {
$fields = array();
$fields['msgid'] = array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
);
$fields += $source_id_schema;
$fields['level'] = array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
);
$fields['message'] = array(
'type' => 'text',
'size' => 'medium',
'not null' => TRUE,
);
$schema = array(
'description' => 'Messages generated during a migration process',
'fields' => $fields,
'primary key' => array('msgid'),
);
if ($pks) {
$schema['indexes']['sourcekey'] = $pks;
}
$this->getDatabase()->schema()->createTable($this->messageTableName(), $schema);
}
}
else {
// Add any missing columns to the map table.
if (!$this->getDatabase()->schema()->fieldExists($this->mapTableName,
'rollback_action')) {
$this->getDatabase()->schema()->addField($this->mapTableName,
'rollback_action', array(
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'Flag indicating what to do for this item on rollback',
));
}
if (!$this->getDatabase()->schema()->fieldExists($this->mapTableName, 'hash')) {
$this->getDatabase()->schema()->addField($this->mapTableName, 'hash', array(
'type' => 'varchar',
'length' => '64',
'not null' => FALSE,
'description' => 'Hash of source row data, for detecting changes',
));
}
}
}
/**
* Create schema from an id definition.
*
* @param array $id_definition
* A field schema definition. Can be SQL schema or a type data
* based schema. In the latter case, the value of type needs to be
* $typed_data_type.$column
* @return array
*/
protected function getFieldSchema(array $id_definition) {
$type_parts = explode('.', $id_definition['type']);
if (count($type_parts) == 1) {
$type_parts[] = 'value';
}
$schema = BaseFieldDefinition::create($type_parts[0])->getColumns();
return $schema[$type_parts[1]];
}
/**
* {@inheritdoc}
*/
public function getRowBySource(array $source_id_values) {
$query = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map');
foreach ($this->sourceIdFields() as $source_id) {
$query = $query->condition("map.$source_id", array_shift($source_id_values), '=');
}
$result = $query->execute();
return $result->fetchAssoc();
}
/**
* {@inheritdoc}
*/
public function getRowByDestination(array $destination_id_values) {
$query = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map');
foreach ($this->destinationIdFields() as $destination_id) {
$query = $query->condition("map.$destination_id", array_shift($destination_id_values), '=');
}
$result = $query->execute();
return $result->fetchAssoc();
}
/**
* {@inheritdoc}
*/
public function getRowsNeedingUpdate($count) {
$rows = array();
$result = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map')
->condition('source_row_status', MigrateIdMapInterface::STATUS_NEEDS_UPDATE)
->range(0, $count)
->execute();
foreach ($result as $row) {
$rows[] = $row;
}
return $rows;
}
/**
* {@inheritdoc}
*/
public function lookupSourceID(array $destination_id) {
$query = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map', $this->sourceIdFields());
foreach ($this->destinationIdFields() as $key_name) {
$query = $query->condition("map.$key_name", array_shift($destination_id), '=');
}
$result = $query->execute();
$source_id = $result->fetchAssoc();
return array_values($source_id ?: array());
}
/**
* {@inheritdoc}
*/
public function lookupDestinationId(array $source_id) {
if (empty($source_id)) {
return array();
}
$query = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map', $this->destinationIdFields());
foreach ($this->sourceIdFields() as $key_name) {
$query = $query->condition("map.$key_name", array_shift($source_id), '=');
}
$result = $query->execute();
$destination_id = $result->fetchAssoc();
return array_values($destination_id ?: array());
}
/**
* {@inheritdoc}
*/
public function saveIdMapping(Row $row, array $destination_id_values, $source_row_status = MigrateIdMapInterface::STATUS_IMPORTED, $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE) {
// Construct the source key.
$source_id_values = $row->getSourceIdValues();
// Construct the source key and initialize to empty variable keys.
$keys = array();
foreach ($this->sourceIdFields() as $field_name => $key_name) {
// A NULL key value will fail.
if (!isset($source_id_values[$field_name])) {
$this->message->display(t(
'Could not save to map table due to NULL value for key field !field',
array('!field' => $field_name)), 'error');
return;
}
$keys[$key_name] = $source_id_values[$field_name];
}
$fields = array(
'source_row_status' => (int) $source_row_status,
'rollback_action' => (int) $rollback_action,
'hash' => $row->getHash(),
);
$count = 0;
foreach ($destination_id_values as $dest_id) {
$fields['destid' . ++$count] = $dest_id;
}
if ($count && $count != count($this->destinationIdFields())) {
$this->message->display(t('Could not save to map table due to missing destination id values'), 'error');
return;
}
if ($this->migration->get('trackLastImported')) {
$fields['last_imported'] = time();
}
if ($keys) {
$this->getDatabase()->merge($this->mapTableName())
->key($keys)
->fields($fields)
->execute();
}
}
/**
* {@inheritdoc}
*/
public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR) {
$count = 1;
foreach ($source_id_values as $id_value) {
$fields['sourceid' . $count++] = $id_value;
// If any key value is not set, we can't save.
if (!isset($id_value)) {
return;
}
}
$fields['level'] = $level;
$fields['message'] = $message;
$this->getDatabase()->insert($this->messageTableName())
->fields($fields)
->execute();
}
/**
* {@inheritdoc}
*/
public function prepareUpdate() {
$this->getDatabase()->update($this->mapTableName())
->fields(array('source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE))
->execute();
}
/**
* {@inheritdoc}
*/
public function processedCount() {
return $this->getDatabase()->select($this->mapTableName())
->countQuery()
->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function importedCount() {
return $this->getDatabase()->select($this->mapTableName())
->condition('source_row_status', array(MigrateIdMapInterface::STATUS_IMPORTED, MigrateIdMapInterface::STATUS_NEEDS_UPDATE), 'IN')
->countQuery()
->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function updateCount() {
return $this->countHelper(MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
}
/**
* {@inheritdoc}
*/
public function errorCount() {
return $this->countHelper(MigrateIdMapInterface::STATUS_FAILED);
}
/**
* {@inheritdoc}
*/
public function messageCount() {
return $this->countHelper(NULL, $this->messageTableName());
}
/**
* Counts records in a table.
*
* @param $status
* An integer for the source_row_status column.
* @param $table
* The table to work
* @return int
* The number of records.
*/
protected function countHelper($status, $table = NULL) {
$query = $this->getDatabase()->select($table ?: $this->mapTableName());
if (isset($status)) {
$query->condition('source_row_status', $status);
}
return $query->countQuery()->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function delete(array $source_id_values, $messages_only = FALSE) {
if (empty($source_id_values)) {
throw new MigrateException('Without source identifier values it is impossible to find the row to delete.');
}
if (!$messages_only) {
$map_query = $this->getDatabase()->delete($this->mapTableName());
}
$message_query = $this->getDatabase()->delete($this->messageTableName());
$count = 1;
foreach ($source_id_values as $id_value) {
if (!$messages_only) {
$map_query->condition('sourceid' . $count, $id_value);
}
$message_query->condition('sourceid' . $count, $id_value);
$count++;
}
if (!$messages_only) {
$map_query->execute();
}
$message_query->execute();
}
/**
* {@inheritdoc}
*/
public function deleteDestination(array $destination_id) {
$map_query = $this->getDatabase()->delete($this->mapTableName());
$message_query = $this->getDatabase()->delete($this->messageTableName());
$source_id = $this->lookupSourceID($destination_id);
if (!empty($source_id)) {
$count = 1;
foreach ($destination_id as $key_value) {
$map_query->condition('destid' . $count, $key_value);
$count++;
}
$map_query->execute();
$count = 1;
foreach ($source_id as $key_value) {
$message_query->condition('sourceid' . $count, $key_value);
$count++;
}
$message_query->execute();
}
}
/**
* {@inheritdoc}
*/
public function setUpdate(array $source_id) {
if (empty($source_id)) {
throw new MigrateException('No source identifiers provided to update.');
}
$query = $this->getDatabase()
->update($this->mapTableName())
->fields(array('source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE));
$count = 1;
foreach ($source_id as $key_value) {
$query->condition('sourceid' . $count++, $key_value);
}
$query->execute();
}
/**
* {@inheritdoc}
*/
public function deleteBulk(array $source_id_values) {
// If we have a single-column key, we can shortcut it.
if (count($this->migration->getSourcePlugin()->getIds()) == 1) {
$sourceids = array();
foreach ($source_id_values as $source_id) {
$sourceids[] = $source_id;
}
$this->getDatabase()->delete($this->mapTableName())
->condition('sourceid1', $sourceids, 'IN')
->execute();
$this->getDatabase()->delete($this->messageTableName())
->condition('sourceid1', $sourceids, 'IN')
->execute();
}
else {
foreach ($source_id_values as $source_id) {
$map_query = $this->getDatabase()->delete($this->mapTableName());
$message_query = $this->getDatabase()->delete($this->messageTableName());
$count = 1;
foreach ($source_id as $key_value) {
$map_query->condition('sourceid' . $count, $key_value);
$message_query->condition('sourceid' . $count++, $key_value);
}
$map_query->execute();
$message_query->execute();
}
}
}
/**
* {@inheritdoc}
*/
public function clearMessages() {
$this->getDatabase()->truncate($this->messageTableName())->execute();
}
/**
* {@inheritdoc}
*/
public function destroy() {
$this->getDatabase()->schema()->dropTable($this->mapTableName());
$this->getDatabase()->schema()->dropTable($this->messageTableName());
}
/**
* Implementation of Iterator::rewind().
*
* This is called before beginning a foreach loop.
*
* @todo Support idlist, itemlimit.
*/
public function rewind() {
$this->currentRow = NULL;
$fields = array();
foreach ($this->sourceIdFields() as $field) {
$fields[] = $field;
}
foreach ($this->destinationIdFields() as $field) {
$fields[] = $field;
}
// @todo Make this work.
/*
if (isset($this->options['itemlimit'])) {
$query = $query->range(0, $this->options['itemlimit']);
}
*/
$this->result = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map', $fields)
->execute();
$this->next();
}
/**
* Implementation of Iterator::current().
*
* This is called when entering a loop iteration, returning the current row.
*/
public function current() {
return $this->currentRow;
}
/**
* Implementation of Iterator::key().
*
* This is called when entering a loop iteration, returning the key of the
* current row. It must be a scalar - we will serialize to fulfill the
* requirement, but using getCurrentKey() is preferable.
*/
public function key() {
return serialize($this->currentKey);
}
/**
* Implementation of Iterator::next().
*
* This is called at the bottom of the loop implicitly, as well as explicitly
* from rewind().
*/
public function next() {
$this->currentRow = $this->result->fetchAssoc();
$this->currentKey = array();
if ($this->currentRow) {
foreach ($this->sourceIdFields() as $map_field) {
$this->currentKey[$map_field] = $this->currentRow[$map_field];
// Leave only destination fields.
unset($this->currentRow[$map_field]);
}
}
}
/**
* Implementation of Iterator::valid().
*
* This is called at the top of the loop, returning TRUE to process the loop
* and FALSE to terminate it.
*/
public function valid() {
// @todo Check numProcessed against itemlimit.
return $this->currentRow !== FALSE;
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Callback.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* This plugin allows source value to be passed to a callback.
*
* The current value is passed to a callable that returns the processed value.
* This plugin allows simple processing of the value, such as strtolower(). The
* callable takes the value as the single mandatory argument. No additional
* arguments can be passed to the callback as this would make the migration YAML
* file too complex.
*
* @MigrateProcessPlugin(
* id = "callback"
* )
*/
class Callback extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_callable($this->configuration['callable'])) {
$value = call_user_func($this->configuration['callable'], $value);
}
return $value;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Concat.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Concatenates the strings in the current value.
*
* @MigrateProcessPlugin(
* id = "concat",
* handle_multiples = TRUE
* )
*/
class Concat extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Concatenates the strings in the current value.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_array($value)) {
$delimiter = isset($this->configuration['delimiter']) ? $this->configuration['delimiter'] : '';
return implode($delimiter, $value);
}
else {
throw new MigrateException(sprintf('%s is not an array', SafeMarkup::checkPlain(var_export($value, TRUE))));
}
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\DedupeBase.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateException;
use Drupal\Component\Utility\Unicode;
/**
* This abstract base contains the dedupe logic.
*
* These plugins avoid duplication at the destination. For example, when
* creating filter format names, the current value is checked against the
* existing filter format names and if it exists, a numeric postfix is added
* and incremented until a unique value is created.
*/
abstract class DedupeBase extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$i = 1;
$postfix = isset($this->configuration['postfix']) ? $this->configuration['postfix'] : '';
$start = isset($this->configuration['start']) ? $this->configuration['start'] : 0;
if (!is_int($start)) {
throw new MigrateException('The start position configuration key should be an integer. Omit this key to capture from the beginning of the string.');
}
$length = isset($this->configuration['length']) ? $this->configuration['length'] : NULL;
if (!is_null($length) && !is_int($length)) {
throw new MigrateException('The character length configuration key should be an integer. Omit this key to capture the entire string.');
}
// Use optional start or length to return a portion of deduplicated value.
$value = Unicode::substr($value, $start, $length);
$new_value = $value;
while ($this->exists($new_value)) {
$new_value = $value . $postfix . $i++;
}
return $new_value;
}
/**
* This is a query checking the existence of some value.
*
* @param mixed $value
* The value to check.
*
* @return bool
* TRUE if the value exists.
*/
abstract protected function exists($value);
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\DedupeEntity.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Ensures value is not duplicated against an entity field.
*
* @MigrateProcessPlugin(
* id = "dedupe_entity"
* )
*/
class DedupeEntity extends DedupeBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\Core\Entity\Query\QueryFactoryInterface
*/
protected $entityQueryFactory;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, QueryFactory $entity_query_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityQueryFactory = $entity_query_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity.query')
);
}
/**
* {@inheritdoc}
*/
protected function exists($value) {
// Plugins are cached so for every run we need a new query object.
return $this
->entityQueryFactory
->get($this->configuration['entity_type'], 'AND')
->condition($this->configuration['field'], $value)
->count()
->execute();
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\DefaultValue.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* This plugin sets missing values on the destination.
*
* @MigrateProcessPlugin(
* id = "default_value"
* )
*/
class DefaultValue extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!empty($this->configuration['strict'])) {
return isset($value) ? $value : $this->configuration['default_value'];
}
return $value ?: $this->configuration['default_value'];
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Extract.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Component\Utility\NestedArray;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* This plugin extracts a value from an array.
*
* @see https://www.drupal.org/node/2152731
*
* @MigrateProcessPlugin(
* id = "extract"
* )
*/
class Extract extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!is_array($value)) {
throw new MigrateException('Input should be an array.');
}
$new_value = NestedArray::getValue($value, $this->configuration['index'], $key_exists);
if (!$key_exists) {
throw new MigrateException('Array index missing, extraction failed.');
}
return $new_value;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Flatten.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* This plugin flattens the current value.
*
* During some types of processing (e.g. user permission splitting), what was
* once a single value gets transformed into multiple values. This plugin will
* flatten them back down to single values again.
*
* @see https://www.drupal.org/node/2154215
*
* @MigrateProcessPlugin(
* id = "flatten",
* handle_multiples = TRUE
* )
*/
class Flatten extends ProcessPluginBase {
/**
* Flatten nested array values to single array values.
*
* For example, array(array(1, 2, array(3, 4))) becomes array(1, 2, 3, 4).
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
return iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($value)), FALSE);
}
}

View file

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Get.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* This plugin copies from the source to the destination.
*
* @MigrateProcessPlugin(
* id = "get"
* )
*/
class Get extends ProcessPluginBase {
/**
* @var bool
*/
protected $multiple;
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$source = $this->configuration['source'];
$properties = is_string($source) ? array($source) : $source;
$return = array();
foreach ($properties as $property) {
if (empty($property)) {
$return[] = $value;
}
else {
$is_source = TRUE;
if ($property[0] == '@') {
$property = preg_replace_callback('/^(@?)((?:@@)*)([^@]|$)/', function ($matches) use (&$is_source) {
// If there are an odd number of @ in the beginning, it's a
// destination.
$is_source = empty($matches[1]);
// Remove the possible escaping and do not lose the terminating
// non-@ either.
return str_replace('@@', '@', $matches[2]) . $matches[3];
}, $property);
}
if ($is_source) {
$return[] = $row->getSourceProperty($property);
}
else {
$return[] = $row->getDestinationProperty($property);
}
}
}
if (is_string($source)) {
$this->multiple = is_array($return[0]);
return $return[0];
}
return $return;
}
/**
* {@inheritdoc}
*/
public function multiple() {
return $this->multiple;
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Iterator.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* This plugin iterates and processes an array.
*
* @see https://www.drupal.org/node/2135345
*
* @MigrateProcessPlugin(
* id = "iterator",
* handle_multiples = TRUE
* )
*/
class Iterator extends ProcessPluginBase {
/**
* Runs a process pipeline on each destination property per list item.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$return = array();
foreach ($value as $key => $new_value) {
$new_row = new Row($new_value, array());
$migrate_executable->processRow($new_row, $this->configuration['process']);
$destination = $new_row->getDestination();
if (array_key_exists('key', $this->configuration)) {
$key = $this->transformKey($key, $migrate_executable, $new_row);
}
$return[$key] = $destination;
}
return $return;
}
/**
* Runs the process pipeline for the current key.
*
* @param string|int $key
* The current key.
* @param \Drupal\migrate\MigrateExecutableInterface $migrate_executable
* The migrate executable helper class.
* @param \Drupal\migrate\Row $row
* The current row after processing.
*
* @return mixed
* The transformed key.
*/
protected function transformKey($key, MigrateExecutableInterface $migrate_executable, Row $row) {
$process = array('key' => $this->configuration['key']);
$migrate_executable->processRow($row, $process, $key);
return $row->getDestinationProperty('key');
}
/**
* {@inheritdoc}
*/
public function multiple() {
return TRUE;
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\MachineName.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Component\Transliteration\TransliterationInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* This plugin creates a machine name.
*
* The current value gets transliterated, non-alphanumeric characters removed
* and replaced by an underscore and multiple underscores are collapsed into
* one.
*
* @MigrateProcessPlugin(
* id = "machine_name"
* )
*/
class MachineName extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\Component\Transliteration\TransliterationInterface
*/
protected $transliteration;
/**
* Constructs a MachineName plugin.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration
* The transliteration service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, TransliterationInterface $transliteration) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->transliteration = $transliteration;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('transliteration')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$new_value = $this->transliteration->transliterate($value, LanguageInterface::LANGCODE_DEFAULT, '_');
$new_value = strtolower($new_value);
$new_value = preg_replace('/[^a-z0-9_]+/', '_', $new_value);
return preg_replace('/_+/', '_', $new_value);
}
}

View file

@ -0,0 +1,165 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Migration.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateSkipProcessException;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\MigratePluginManager;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Calculates the value of a property based on a previous migration.
*
* @MigrateProcessPlugin(
* id = "migration"
* )
*/
class Migration extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $processPluginManager;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $migrationStorage;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, MigratePluginManager $process_plugin_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migrationStorage = $storage;
$this->migration = $migration;
$this->processPluginManager = $process_plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity.manager')->getStorage('migration'),
$container->get('plugin.manager.migrate.process')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$migration_ids = $this->configuration['migration'];
if (!is_array($migration_ids)) {
$migration_ids = array($migration_ids);
}
$scalar = FALSE;
if (!is_array($value)) {
$scalar = TRUE;
$value = array($value);
}
$this->skipOnEmpty($value);
$self = FALSE;
/** @var \Drupal\migrate\Entity\MigrationInterface[] $migrations */
$migrations = $this->migrationStorage->loadMultiple($migration_ids);
$destination_ids = NULL;
$source_id_values = array();
foreach ($migrations as $migration_id => $migration) {
if ($migration_id == $this->migration->id()) {
$self = TRUE;
}
if (isset($this->configuration['source_ids'][$migration_id])) {
$configuration = array('source' => $this->configuration['source_ids'][$migration_id]);
$source_id_values[$migration_id] = $this->processPluginManager
->createInstance('get', $configuration, $this->migration)
->transform(NULL, $migrate_executable, $row, $destination_property);
}
else {
$source_id_values[$migration_id] = $value;
}
// Break out of the loop as soon as a destination ID is found.
if ($destination_ids = $migration->getIdMap()->lookupDestinationID($source_id_values[$migration_id])) {
break;
}
}
if (!$destination_ids && ($self || isset($this->configuration['stub_id']) || count($migrations) == 1)) {
// If the lookup didn't succeed, figure out which migration will do the
// stubbing.
if ($self) {
$migration = $this->migration;
}
elseif (isset($this->configuration['stub_id'])) {
$migration = $migrations[$this->configuration['stub_id']];
}
else {
$migration = reset($migrations);
}
$destination_plugin = $migration->getDestinationPlugin(TRUE);
// Only keep the process necessary to produce the destination ID.
$process = $migration->get('process');
// We already have the source id values but need to key them for the Row
// constructor.
$source_ids = $migration->getSourcePlugin()->getIds();
$values = array();
foreach (array_keys($source_ids) as $index => $source_id) {
$values[$source_id] = $source_id_values[$migration->id()][$index];
}
$stub_row = new Row($values + $migration->get('source'), $source_ids, TRUE);
// Do a normal migration with the stub row.
$migrate_executable->processRow($stub_row, $process);
$destination_ids = array();
try {
$destination_ids = $destination_plugin->import($stub_row);
}
catch (\Exception $e) {
$migrate_executable->saveMessage($e->getMessage());
}
}
if ($destination_ids) {
if ($scalar) {
if (count($destination_ids) == 1) {
return reset($destination_ids);
}
}
else {
return $destination_ids;
}
}
throw new MigrateSkipRowException();
}
/**
* Skip the migration process entirely if the value is FALSE.
*
* @param mixed $value
* The incoming value to transform.
*
* @throws \Drupal\migrate\MigrateSkipProcessException
*/
protected function skipOnEmpty($value) {
if (!array_filter($value)) {
throw new MigrateSkipProcessException();
}
}
}

View file

@ -0,0 +1,93 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Route.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* @MigrateProcessPlugin(
* id = "route"
* )
*/
class Route extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, PathValidatorInterface $pathValidator) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$this->pathValidator = $pathValidator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('path.validator')
);
}
/**
* {@inheritdoc}
*
* Set the destination route information based on the source link_path.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
list($link_path, $options) = $value;
$extracted = $this->pathValidator->getUrlIfValidWithoutAccessCheck($link_path);
$route = array();
if ($extracted) {
if ($extracted->isExternal()) {
$route['route_name'] = null;
$route['route_parameters'] = array();
$route['options'] = $options;
$route['url'] = $extracted->getUri();
}
else {
$route['route_name'] = $extracted->getRouteName();
$route['route_parameters'] = $extracted->getRouteParameters();
$route['options'] = $extracted->getOptions();
if (isset($options['query'])) {
// If the querystring is stored as a string (as in D6), convert it
// into an array.
if (is_string($options['query'])) {
parse_str($options['query'], $old_query);
}
else {
$old_query = $options['query'];
}
$options['query'] = $route['options']['query'] + $old_query;
unset($route['options']['query']);
}
$route['options'] = $route['options'] + $options;
$route['url'] = null;
}
}
return $route;
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\SkipOnEmpty.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\MigrateSkipProcessException;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateSkipRowException;
/**
* If the source evaluates to empty, we skip processing or the whole row.
*
* @MigrateProcessPlugin(
* id = "skip_on_empty"
* )
*/
class SkipOnEmpty extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function row($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!$value) {
throw new MigrateSkipRowException();
}
return $value;
}
/**
* {@inheritdoc}
*/
public function process($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!$value) {
throw new MigrateSkipProcessException();
}
return $value;
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\SkipRowIfNotSet.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateSkipRowException;
/**
* If the source evaluates to empty, we skip the current row.
*
* @MigrateProcessPlugin(
* id = "skip_row_if_not_set",
* handle_multiples = TRUE
* )
*/
class SkipRowIfNotSet extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!isset($value[$this->configuration['index']])) {
throw new MigrateSkipRowException();
}
return $value[$this->configuration['index']];
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\StaticMap.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Component\Utility\NestedArray;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateSkipRowException;
/**
* This plugin changes the current value based on a static lookup map.
*
* @see https://www.drupal.org/node/2143521
*
* @MigrateProcessPlugin(
* id = "static_map"
* )
*/
class StaticMap extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$new_value = $value;
if (is_array($value)) {
if (!$value) {
throw new MigrateException('Can not lookup without a value.');
}
}
else {
$new_value = array($value);
}
$new_value = NestedArray::getValue($this->configuration['map'], $new_value, $key_exists);
if (!$key_exists) {
if (isset($this->configuration['default_value'])) {
if (!empty($this->configuration['bypass'])) {
throw new MigrateException('Setting both default_value and bypass is invalid.');
}
return $this->configuration['default_value'];
}
if (empty($this->configuration['bypass'])) {
throw new MigrateSkipRowException();
}
else {
return $value;
}
}
return $new_value;
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\source\EmptySource.
*/
namespace Drupal\migrate\Plugin\migrate\source;
/**
* Source returning an empty row.
*
* This is generally useful when needing to create a field using a migration..
*
* @MigrateSource(
* id = "empty"
* )
*/
class EmptySource extends SourcePluginBase {
/**
* {@inheritdoc}
*/
public function fields() {
return array(
'id' => t('ID'),
);
}
/**
* {@inheritdoc}
*/
public function initializeIterator() {
return new \ArrayIterator(array(array('id' => '')));
}
public function __toString() {
return '';
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['id']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function count() {
return 1;
}
}

View file

@ -0,0 +1,458 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\source\SourcePluginBase.
*/
namespace Drupal\migrate\Plugin\migrate\source;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
/**
* The base class for all source plugins.
*
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Annotation\MigrateSource
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see plugin_api
*
* @ingroup migration
*/
abstract class SourcePluginBase extends PluginBase implements MigrateSourceInterface {
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var \Drupal\migrate\Entity\MigrationInterface
*/
protected $migration;
/**
* The name and type of the highwater property in the source.
*
* @var array
*
* @see $originalHighwater
*/
protected $highWaterProperty;
/**
* The current row from the query
*
* @var \Drupal\Migrate\Row
*/
protected $currentRow;
/**
* The primary key of the current row
*
* @var array
*/
protected $currentSourceIds;
/**
* Number of rows intentionally ignored (prepareRow() returned FALSE)
*
* @var int
*/
protected $numIgnored = 0;
/**
* Number of rows we've at least looked at.
*
* @var int
*/
protected $numProcessed = 0;
/**
* The high water mark at the beginning of the import operation.
*
* If the source has a property for tracking changes (like Drupal ha
* node.changed) then this is the highest value of those imported so far.
*
* @var int
*/
protected $originalHighWater;
/**
* List of source IDs to process.
*
* @var array
*/
protected $idList = array();
/**
* Whether this instance should cache the source count.
*
* @var bool
*/
protected $cacheCounts = FALSE;
/**
* Key to use for caching counts.
*
* @var string
*/
protected $cacheKey;
/**
* Whether this instance should not attempt to count the source.
*
* @var bool
*/
protected $skipCount = FALSE;
/**
* If TRUE, we will maintain hashed source rows to determine whether incoming
* data has changed.
*
* @var bool
*/
protected $trackChanges = FALSE;
/**
* By default, next() will directly read the map row and add it to the data
* row. A source plugin implementation may do this itself (in particular, the
* SQL source can incorporate the map table into the query) - if so, it should
* set this TRUE so we don't duplicate the effort.
*
* @var bool
*/
protected $mapRowAdded = FALSE;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $idMap;
/**
* @var \Iterator
*/
protected $iterator;
// @TODO, find out how to remove this.
// @see https://www.drupal.org/node/2443617
public $migrateExecutable;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
// Set up some defaults based on the source configuration.
$this->cacheCounts = !empty($configuration['cache_counts']);
$this->skipCount = !empty($configuration['skip_count']);
$this->cacheKey = !empty($configuration['cache_key']) ? !empty($configuration['cache_key']) : NULL;
$this->trackChanges = !empty($configuration['track_changes']) ? $configuration['track_changes'] : FALSE;
// Pull out the current highwater mark if we have a highwater property.
if ($this->highWaterProperty = $this->migration->get('highWaterProperty')) {
$this->originalHighWater = $this->migration->getHighWater();
}
if ($id_list = $this->migration->get('idlist')) {
$this->idList = $id_list;
}
// Don't allow the use of both highwater and track changes together.
if ($this->highWaterProperty && $this->trackChanges) {
throw new MigrateException('You should either use a highwater mark or track changes not both. They are both designed to solve the same problem');
}
}
/**
* Initialize the iterator with the source data.
*
* @return array
* An array of the data for this source.
*/
protected abstract function initializeIterator();
/**
* Get the module handler.
*
* @return \Drupal\Core\Extension\ModuleHandlerInterface
* The module handler.
*/
protected function getModuleHandler() {
if (!isset($this->moduleHandler)) {
$this->moduleHandler = \Drupal::moduleHandler();
}
return $this->moduleHandler;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$result = TRUE;
$result_hook = $this->getModuleHandler()->invokeAll('migrate_prepare_row', array($row, $this, $this->migration));
$result_named_hook = $this->getModuleHandler()->invokeAll('migrate_' . $this->migration->id() . '_prepare_row', array($row, $this, $this->migration));
// We're explicitly skipping this row - keep track in the map table.
if (($result_hook && in_array(FALSE, $result_hook)) || ($result_named_hook && in_array(FALSE, $result_named_hook))) {
// Make sure we replace any previous messages for this item with any
// new ones.
$id_map = $this->migration->getIdMap();
$id_map->delete($this->currentSourceIds, TRUE);
$this->migrateExecutable->saveQueuedMessages();
$id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_IGNORED, $this->migrateExecutable->rollbackAction);
$this->numIgnored++;
$this->currentRow = NULL;
$this->currentSourceIds = NULL;
$result = FALSE;
}
elseif ($this->trackChanges) {
// When tracking changed data, We want to quietly skip (rather than
// "ignore") rows with changes. The caller needs to make that decision,
// so we need to provide them with the necessary information (before and
// after hashes).
$row->rehash();
}
$this->numProcessed++;
return $result;
}
/**
* Returns the iterator that will yield the row arrays to be processed.
*
* @return \Iterator
*/
public function getIterator() {
if (!isset($this->iterator)) {
$this->iterator = $this->initializeIterator();
}
return $this->iterator;
}
/**
* {@inheritdoc}
*/
public function current() {
return $this->currentRow;
}
/**
* Get the iterator key.
*
* Implementation of Iterator::key - called when entering a loop iteration,
* returning the key of the current row. It must be a scalar - we will
* serialize to fulfill the requirement, but using getCurrentIds() is
* preferable.
*/
public function key() {
return serialize($this->currentSourceIds);
}
/**
* Whether the iterator is currently valid.
*
* Implementation of Iterator::valid() - called at the top of the loop,
* returning TRUE to process the loop and FALSE to terminate it
*/
public function valid() {
return isset($this->currentRow);
}
/**
* Rewind the iterator.
*
* Implementation of Iterator::rewind() - subclasses of MigrateSource should
* implement performRewind() to do any class-specific setup for iterating
* source records.
*/
public function rewind() {
$this->idMap = $this->migration->getIdMap();
$this->numProcessed = 0;
$this->numIgnored = 0;
$this->getIterator()->rewind();
$this->next();
}
/**
* {@inheritdoc}
*
* The migration iterates over rows returned by the source plugin. This
* method determines the next row which will be processed and imported into
* the system.
*
* The method tracks the source and destination IDs using the ID map plugin.
*
* This also takes care about highwater support. Highwater allows to reimport
* rows from a previous migration run, which got changed in the meantime.
* This is done by specifying a highwater field, which is compared with the
* last time, the migration got executed (originalHighWater).
*/
public function next() {
$this->currentSourceIds = NULL;
$this->currentRow = NULL;
// In order to find the next row we want to process, we ask the source
// plugin for the next possible row.
while (!isset($this->currentRow) && $this->getIterator()->valid()) {
$row_data = $this->getIterator()->current() + $this->configuration;
$this->getIterator()->next();
$row = new Row($row_data, $this->migration->getSourcePlugin()->getIds(), $this->migration->get('destinationIds'));
// Populate the source key for this row.
$this->currentSourceIds = $row->getSourceIdValues();
// Pick up the existing map row, if any, unless getNextRow() did it.
if (!$this->mapRowAdded && ($id_map = $this->idMap->getRowBySource($this->currentSourceIds))) {
$row->setIdMap($id_map);
}
// In case we have specified an ID list, but the ID given by the source is
// not in there, we skip the row.
$id_in_the_list = $this->idList && in_array(reset($this->currentSourceIds), $this->idList);
if ($this->idList && !$id_in_the_list) {
continue;
}
// Preparing the row gives source plugins the chance to skip.
if ($this->prepareRow($row) === FALSE) {
continue;
}
// Check whether the row needs processing.
// 1. Explicitly specified IDs.
// 2. This row has not been imported yet.
// 3. Explicitly set to update.
// 4. The row is newer than the current highwater mark.
// 5. If no such property exists then try by checking the hash of the row.
if ($id_in_the_list || !$row->getIdMap() || $row->needsUpdate() || $this->aboveHighwater($row) || $this->rowChanged($row) ) {
$this->currentRow = $row->freezeSource();
}
}
}
/**
* Check if the incoming data is newer than what we've previously imported.
*
* @param \Drupal\migrate\Row $row
* The row we're importing.
*
* @return bool
* TRUE if the highwater value in the row is greater than our current value.
*/
protected function aboveHighwater(Row $row) {
return $this->highWaterProperty && $row->getSourceProperty($this->highWaterProperty['name']) > $this->originalHighWater;
}
/**
* Check if the incoming row has changed since our last import.
*
* @param \Drupal\migrate\Row $row
* The row we're importing.
*
* @return bool
* TRUE if the row has changed otherwise FALSE.
*/
protected function rowChanged(Row $row) {
return $this->trackChanges && $row->changed();
}
/**
* Getter for currentSourceIds data member.
*/
public function getCurrentIds() {
return $this->currentSourceIds;
}
/**
* Getter for numIgnored data member.
*/
public function getIgnored() {
return $this->numIgnored;
}
/**
* Getter for numProcessed data member.
*/
public function getProcessed() {
return $this->numProcessed;
}
/**
* Reset numIgnored back to 0.
*/
public function resetStats() {
$this->numIgnored = 0;
}
/**
* Get the source count.
*
* Return a count of available source records, from the cache if appropriate.
* Returns -1 if the source is not countable.
*
* @param bool $refresh
* Whether or not to refresh the count.
*
* @return int
* The count.
*/
public function count($refresh = FALSE) {
if ($this->skipCount) {
return -1;
}
if (!isset($this->cacheKey)) {
$this->cacheKey = hash('sha256', $this->getPluginId());
}
// If a refresh is requested, or we're not caching counts, ask the derived
// class to get the count from the source.
if ($refresh || !$this->cacheCounts) {
$count = $this->getIterator()->count();
$this->getCache()->set($this->cacheKey, $count, 'cache');
}
else {
// Caching is in play, first try to retrieve a cached count.
$cache_object = $this->getCache()->get($this->cacheKey, 'cache');
if (is_object($cache_object)) {
// Success.
$count = $cache_object->data;
}
else {
// No cached count, ask the derived class to count 'em up, and cache
// the result.
$count = $this->getIterator()->count();
$this->getCache()->set($this->cacheKey, $count, 'cache');
}
}
return $count;
}
/**
* Get the cache object.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* The cache object.
*/
protected function getCache() {
if (!isset($this->cache)) {
$this->cache = \Drupal::cache('migrate');
}
return $this->cache;
}
}

View file

@ -0,0 +1,235 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\source\SqlBase.
*/
namespace Drupal\migrate\Plugin\migrate\source;
use Drupal\Core\Database\Database;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\migrate\id_map\Sql;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Sources whose data may be fetched via DBTNG.
*
* By default, an existing database connection with key 'migrate' and target
* 'default' is used. These may be overridden with explicit 'key' and/or
* 'target' configuration keys. In addition, if the configuration key 'database'
* is present, it is used as a database connection information array to define
* the connection.
*/
abstract class SqlBase extends SourcePluginBase {
/**
* @var \Drupal\Core\Database\Query\SelectInterface
*/
protected $query;
/**
* @var \Drupal\migrate\Entity\MigrationInterface
*/
protected $migration;
/**
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
}
/**
* Print the query string when the object is used a string.
*
* @return string
* The query string.
*/
public function __toString() {
return (string) $this->query;
}
/**
* Get the database connection object.
*
* @return \Drupal\Core\Database\Connection
* The database connection.
*/
public function getDatabase() {
if (!isset($this->database)) {
if (isset($this->configuration['target'])) {
$target = $this->configuration['target'];
}
else {
$target = 'default';
}
if (isset($this->configuration['key'])) {
$key = $this->configuration['key'];
}
else {
$key = 'migrate';
}
if (isset($this->configuration['database'])) {
Database::addConnectionInfo($key, $target, $this->configuration['database']);
}
$this->database = Database::getConnection($target, $key);
}
return $this->database;
}
/**
* Wrapper for database select.
*/
protected function select($table, $alias = NULL, array $options = array()) {
$options['fetch'] = \PDO::FETCH_ASSOC;
return $this->getDatabase()->select($table, $alias, $options);
}
/**
* A helper for adding tags and metadata to the query.
*
* @return \Drupal\Core\Database\Query\SelectInterface
* The query with additional tags and metadata.
*/
protected function prepareQuery() {
$this->query = clone $this->query();
$this->query->addTag('migrate');
$this->query->addTag('migrate_' . $this->migration->id());
$this->query->addMetaData('migration', $this->migration);
return $this->query;
}
/**
* Implementation of MigrateSource::performRewind().
*
* We could simply execute the query and be functionally correct, but
* we will take advantage of the PDO-based API to optimize the query up-front.
*/
protected function initializeIterator() {
$this->prepareQuery();
$high_water_property = $this->migration->get('highWaterProperty');
// Get the key values, for potential use in joining to the map table, or
// enforcing idlist.
$keys = array();
// The rules for determining what conditions to add to the query are as
// follows (applying first applicable rule)
// 1. If idlist is provided, then only process items in that list (AND key
// IN (idlist)). Only applicable with single-value keys.
if ($id_list = $this->migration->get('idlist')) {
$this->query->condition($keys[0], $id_list, 'IN');
}
else {
// 2. If the map is joinable, join it. We will want to accept all rows
// which are either not in the map, or marked in the map as NEEDS_UPDATE.
// Note that if high water fields are in play, we want to accept all rows
// above the high water mark in addition to those selected by the map
// conditions, so we need to OR them together (but AND with any existing
// conditions in the query). So, ultimately the SQL condition will look
// like (original conditions) AND (map IS NULL OR map needs update
// OR above high water).
$conditions = $this->query->orConditionGroup();
$condition_added = FALSE;
if ($this->mapJoinable()) {
// Build the join to the map table. Because the source key could have
// multiple fields, we need to build things up.
$count = 1;
$map_join = '';
$delimiter = '';
foreach ($this->getIds() as $field_name => $field_schema) {
if (isset($field_schema['alias'])) {
$field_name = $field_schema['alias'] . '.' . $field_name;
}
$map_join .= "$delimiter$field_name = map.sourceid" . $count++;
$delimiter = ' AND ';
}
$alias = $this->query->leftJoin($this->migration->getIdMap()->getQualifiedMapTableName(), 'map', $map_join);
$conditions->isNull($alias . '.sourceid1');
$conditions->condition($alias . '.source_row_status', MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
$condition_added = TRUE;
// And as long as we have the map table, add its data to the row.
$n = count($this->getIds());
for ($count = 1; $count <= $n; $count++) {
$map_key = 'sourceid' . $count;
$this->query->addField($alias, $map_key, "migrate_map_$map_key");
}
if ($n = count($this->migration->get('destinationIds'))) {
for ($count = 1; $count <= $n; $count++) {
$map_key = 'destid' . $count++;
$this->query->addField($alias, $map_key, "migrate_map_$map_key");
}
}
$this->query->addField($alias, 'source_row_status', 'migrate_map_source_row_status');
}
// 3. If we are using high water marks, also include rows above the mark.
// But, include all rows if the high water mark is not set.
if (isset($high_water_property['name']) && ($high_water = $this->migration->getHighWater()) !== '') {
if (isset($high_water_property['alias'])) {
$high_water = $high_water_property['alias'] . '.' . $high_water_property['name'];
}
else {
$high_water = $high_water_property['name'];
}
$conditions->condition($high_water, $high_water, '>');
$condition_added = TRUE;
}
if ($condition_added) {
$this->query->condition($conditions);
}
}
return new \IteratorIterator($this->query->execute());
}
/**
* @return \Drupal\Core\Database\Query\SelectInterface
*/
abstract public function query();
/**
* {@inheritdoc}
*/
public function count() {
return $this->query()->countQuery()->execute()->fetchField();
}
/**
* Check if we can join against the map table.
*
* This function specifically catches issues when we're migrating with
* unique sets of credentials for the source and destination database.
*
* @return bool
* TRUE if we can join against the map table otherwise FALSE.
*/
protected function mapJoinable() {
if (!$this->getIds()) {
return FALSE;
}
$id_map = $this->migration->getIdMap();
if (!$id_map instanceof Sql) {
return FALSE;
}
$id_map_database_options = $id_map->getDatabase()->getConnectionOptions();
$source_database_options = $this->getDatabase()->getConnectionOptions();
foreach (array('username', 'password', 'host', 'port', 'namespace', 'driver') as $key) {
if (isset($source_database_options[$key])) {
if ($id_map_database_options[$key] != $source_database_options[$key]) {
return FALSE;
}
}
}
return TRUE;
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\migrate\ProcessPluginBase.
*/
namespace Drupal\migrate;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Plugin\MigrateProcessInterface;
/**
* The base class for all migrate process plugins.
*
* Migrate process plugins are taking a value and transform them. For example,
* transform a human provided name into a machine name, look up an identifier
* in a previous migration and so on.
*
* @see https://www.drupal.org/node/2129651
* @see \Drupal\migrate\Plugin\MigratePluginManager
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
* @see \Drupal\migrate\Annotation\MigrateProcessPlugin
* @see plugin_api
*
* @ingroup migration
*/
abstract class ProcessPluginBase extends PluginBase implements MigrateProcessInterface {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// Do not call this method from children.
if (isset($this->configuration['method'])) {
if (method_exists($this, $this->configuration['method'])) {
return $this->{$this->configuration['method']}($value, $migrate_executable, $row, $destination_property);
}
throw new \BadMethodCallException(sprintf('The %s method does not exist in the %s plugin.', $this->configuration['method'], $this->pluginId));
}
else {
throw new \BadMethodCallException(sprintf('The "method" key in the plugin configuration must to be set for the %s plugin.', $this->pluginId));
}
}
/**
* {@inheritdoc}
*/
public function multiple() {
return FALSE;
}
}

View file

@ -0,0 +1,323 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Row.
*/
namespace Drupal\migrate;
use Drupal\Component\Utility\NestedArray;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Stores a row.
*/
class Row {
/**
* The actual values of the source row.
*
* @var array
*/
protected $source = array();
/**
* The source identifiers.
*
* @var array
*/
protected $sourceIds = array();
/**
* The destination values.
*
* @var array
*/
protected $destination = array();
/**
* Level separator of destination and source properties.
*/
const PROPERTY_SEPARATOR = '/';
/**
* The mapping between source and destination identifiers.
*
* @var array
*/
protected $idMap = array(
'original_hash' => '',
'hash' => '',
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
);
/**
* Whether the source has been frozen already.
*
* Once frozen the source can not be changed any more.
*
* @var bool
*/
protected $frozen = FALSE;
/**
* The raw destination properties.
*
* Unlike $destination which is set by using
* \Drupal\Component\Utility\NestedArray::setValue() this array contains
* the destination as setDestinationProperty was called.
*
* @var array
* The raw destination.
*
* @see getRawDestination()
*/
protected $rawDestination;
/**
* TRUE when this row is a stub.
*
* @var bool
*/
protected $isStub = FALSE;
/**
* Constructs a \Drupal\Migrate\Row object.
*
* @param array $values
* An array of values to add as properties on the object.
* @param array $source_ids
* An array containing the IDs of the source using the keys as the field
* names.
* @param bool $is_stub
* TRUE if the row being created is a stub.
*
* @throws \InvalidArgumentException
* Thrown when a source ID property does not exist.
*/
public function __construct(array $values, array $source_ids, $is_stub = FALSE) {
$this->source = $values;
$this->sourceIds = $source_ids;
$this->isStub = $is_stub;
foreach (array_keys($source_ids) as $id) {
if (!$this->hasSourceProperty($id)) {
throw new \InvalidArgumentException("$id has no value");
}
}
}
/**
* Retrieves the values of the source identifiers.
*
* @return array
* An array containing the values of the source identifiers.
*/
public function getSourceIdValues() {
return array_intersect_key($this->source, $this->sourceIds);
}
/**
* Determines whether a source has a property.
*
* @param string $property
* A property on the source.
*
* @return bool
* TRUE if the source has property; FALSE otherwise.
*/
public function hasSourceProperty($property) {
return NestedArray::keyExists($this->source, explode(static::PROPERTY_SEPARATOR, $property));
}
/**
* Retrieves a source property.
*
* @param string $property
* A property on the source.
*
* @return mixed|null
* The found returned property or NULL if not found.
*/
public function getSourceProperty($property) {
$return = NestedArray::getValue($this->source, explode(static::PROPERTY_SEPARATOR, $property), $key_exists);
if ($key_exists) {
return $return;
}
}
/**
* Returns the whole source array.
*
* @return array
* An array of source plugins.
*/
public function getSource() {
return $this->source;
}
/**
* Sets a source property.
*
* This can only be called from the source plugin.
*
* @param string $property
* A property on the source.
* @param mixed $data
* The property value to set on the source.
*
* @throws \Exception
*/
public function setSourceProperty($property, $data) {
if ($this->frozen) {
throw new \Exception("The source is frozen and can't be changed any more");
}
else {
NestedArray::setValue($this->source, explode(static::PROPERTY_SEPARATOR, $property), $data, TRUE);
}
}
/**
* Freezes the source.
*
* @return $this
*/
public function freezeSource() {
$this->frozen = TRUE;
return $this;
}
/**
* Tests if destination property exists.
*
* @param array|string $property
* An array of properties on the destination.
*
* @return bool
* TRUE if the destination property exists.
*/
public function hasDestinationProperty($property) {
return NestedArray::keyExists($this->destination, explode(static::PROPERTY_SEPARATOR, $property));
}
/**
* Sets destination properties.
*
* @param string $property
* The name of the destination property.
* @param mixed $value
* The property value to set on the destination.
*/
public function setDestinationProperty($property, $value) {
$this->rawDestination[$property] = $value;
NestedArray::setValue($this->destination, explode(static::PROPERTY_SEPARATOR, $property), $value, TRUE);
}
/**
* Returns the whole destination array.
*
* @return array
* An array of destination values.
*/
public function getDestination() {
return $this->destination;
}
/**
* Returns the raw destination. Rarely necessary.
*
* For example calling setDestination('foo/bar', 'baz') results in
* @code
* $this->destination['foo']['bar'] = 'baz';
* $this->rawDestination['foo/bar'] = 'baz';
* @endcode
*
* @return array
* The raw destination values.
*/
public function getRawDestination() {
return $this->rawDestination;
}
/**
* Returns the value of a destination property.
*
* @param string $property
* The name of a property on the destination.
*
* @return mixed
* The destination value.
*/
public function getDestinationProperty($property) {
return NestedArray::getValue($this->destination, explode(static::PROPERTY_SEPARATOR, $property));
}
/**
* Sets the Migrate ID mappings.
*
* @param array $id_map
* An array of mappings between source ID and destination ID.
*/
public function setIdMap(array $id_map) {
$this->idMap = $id_map;
}
/**
* Retrieves the Migrate ID mappings.
*
* @return array
* An array of mapping between source and destination identifiers.
*/
public function getIdMap() {
return $this->idMap;
}
/**
* Recalculates the hash for the row.
*/
public function rehash() {
$this->idMap['original_hash'] = $this->idMap['hash'];
$this->idMap['hash'] = hash('sha256', serialize($this->source));
}
/**
* Checks whether the row has changed compared to the original ID map.
*
* @return bool
* TRUE if the row has changed, FALSE otherwise. If setIdMap() was not
* called, this always returns FALSE.
*/
public function changed() {
return $this->idMap['original_hash'] != $this->idMap['hash'];
}
/**
* Returns if this row needs an update.
*
* @return bool
* TRUE if the row needs updating, FALSE otherwise.
*/
public function needsUpdate() {
return $this->idMap['source_row_status'] == MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
}
/**
* Returns the hash for the source values..
*
* @return mixed
* The hash of the source values.
*/
public function getHash() {
return $this->idMap['hash'];
}
/**
* Reports whether this row is a stub.
*
* @return bool
* The current stub value.
*/
public function isStub() {
return $this->isStub;
}
}

View file

@ -0,0 +1,295 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Tests\EntityFileTest.
*/
namespace Drupal\migrate\Tests;
use Drupal\Core\Site\Settings;
use Drupal\migrate\Row;
use Drupal\migrate\Plugin\migrate\destination\EntityFile;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\migrate\MigrateException;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the entity file destination plugin.
*
* @group migrate
*/
class EntityFileTest extends KernelTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('system', 'entity_test', 'user', 'file');
/**
* @var \Drupal\migrate\Tests\TestEntityFile $destination
*/
protected $destination;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->destination = new TestEntityFile([]);
$this->destination->streamWrapperManager = \Drupal::getContainer()->get('stream_wrapper_manager');
$this->destination->fileSystem = \Drupal::getContainer()->get('file_system');
$this->installEntitySchema('file');
file_put_contents('/tmp/test-file.jpg', '');
}
/**
* Test successful imports/copies.
*/
public function testSuccessfulCopies() {
foreach ($this->localFileDataProvider() as $data) {
list($row_values, $destination_path, $expected, $source_base_path) = $data;
$this->doImport($row_values, $destination_path, $source_base_path);
$message = $expected ? sprintf('File %s exists', $destination_path) : sprintf('File %s does not exist', $destination_path);
$this->assertIdentical($expected, is_file($destination_path), $message);
}
}
/**
* The data provider for testing the file destination.
*
* @return array
* An array of file permutations to test.
*/
protected function localFileDataProvider() {
global $base_url;
return [
// Test a local to local copy.
[['filepath' => 'core/modules/simpletest/files/image-test.jpg'], 'public://file1.jpg', TRUE, DRUPAL_ROOT . '/'],
// Test a temporary file using an absolute path.
[['filepath' => '/tmp/test-file.jpg'], 'temporary://test.jpg', TRUE, ''],
// Test a temporary file using a relative path.
[['filepath' => 'test-file.jpg'], 'temporary://core/modules/simpletest/files/test.jpg', TRUE, '/tmp/'],
// Test a remote path to local.
[['filepath' => 'core/modules/simpletest/files/image-test.jpg'], 'public://remote-file.jpg', TRUE, $base_url . '/'],
// Test a remote path to local inside a folder that doesn't exist.
[['filepath' => 'core/modules/simpletest/files/image-test.jpg'], 'public://folder/remote-file.jpg', TRUE, DRUPAL_ROOT . '/'],
];
}
/**
* Test that non-existent files throw an exception.
*/
public function testNonExistentSourceFile() {
$destination = '/non/existent/file';
try {
// If this test passes, doImport() will raise a MigrateException and
// we'll never reach fail().
$this->doImport(['filepath' => $destination], 'public://wontmatter.jpg');
$this->fail('Expected Drupal\migrate\MigrateException when importing ' . $destination);
}
catch (MigrateException $e) {
$this->assertIdentical($e->getMessage(), 'File ' . $destination . ' does not exist.');
}
}
/**
* Tests various invocations of the writeFile() method.
*/
public function testWriteFile() {
$plugin = $this->destination;
$method = new \ReflectionMethod($plugin, 'writeFile');
$method->setAccessible(TRUE);
touch('temporary://baz.txt');
// Moving an actual file should return TRUE.
$plugin->configuration['move'] = TRUE;
$this->assertTrue($method->invoke($plugin, 'temporary://baz.txt', 'public://foo.txt'));
// Trying to move a non-existent file should return FALSE.
$this->assertFalse($method->invoke($plugin, 'temporary://invalid.txt', 'public://invalid.txt'));
// Copying over a file that already exists should replace the existing file.
$plugin->configuration['move'] = FALSE;
touch('temporary://baz.txt');
$this->assertTrue($method->invoke($plugin, 'temporary://baz.txt', 'public://foo.txt'));
// Copying over a file that already exists should rename the resulting file
// if FILE_EXISTS_RENAME is specified.
$method->invoke($plugin, 'temporary://baz.txt', 'public://foo.txt', FILE_EXISTS_RENAME);
$this->assertTrue(file_exists('public://foo_0.txt'));
// Trying to copy a non-existent file should return FALSE.
$this->assertFalse($method->invoke($plugin, 'temporary://invalid.txt', 'public://invalid.txt'));
}
/**
* Tests various invocations of the getOverwriteMode() method.
*/
public function testGetOverwriteMode() {
$plugin = $this->destination;
$method = new \ReflectionMethod($plugin, 'getOverwriteMode');
$method->setAccessible(TRUE);
$row = new Row([], []);
// If the plugin is not configured to rename the destination file, we should
// always get FILE_EXISTS_REPLACE.
$this->assertIdentical(FILE_EXISTS_REPLACE, $method->invoke($plugin, $row));
// When the plugin IS configured to rename the destination file, it should
// return FILE_EXISTS_RENAME if the destination entity already exists,
// and FILE_EXISTS_REPLACE otherwise.
$plugin->configuration['rename'] = TRUE;
$plugin->storage = \Drupal::entityManager()->getStorage('file');
/** @var \Drupal\file\FileInterface $file */
$file = $plugin->storage->create();
touch('public://foo.txt');
$file->setFileUri('public://foo.txt');
$file->save();
$row->setDestinationProperty($plugin->storage->getEntityType()->getKey('id'), $file->id());
$this->assertIdentical(FILE_EXISTS_RENAME, $method->invoke($plugin, $row));
unlink('public://foo.txt');
}
/**
* Tests various invocations of the getDirectory() method.
*/
public function testGetDirectory() {
$plugin = $this->destination;
$method = new \ReflectionMethod($plugin, 'getDirectory');
$method->setAccessible(TRUE);
$this->assertEqual('public://foo', $method->invoke($plugin, 'public://foo/baz.txt'));
$this->assertEqual('/path/to', $method->invoke($plugin, '/path/to/foo.txt'));
// A directory like public:// (no path) needs to resolve to a physical path.
$fs = \Drupal::getContainer()->get('file_system');
$this->assertEqual($fs->realpath(Settings::get('file_public_path')), $method->invoke($plugin, 'public://foo.txt'));
}
/**
* Tests various invocations of the isLocationUnchanged() method.
*/
public function testIsLocationUnchanged() {
$plugin = $this->destination;
$method = new \ReflectionMethod($plugin, 'isLocationUnchanged');
$method->setAccessible(TRUE);
$public_dir = Settings::get('file_public_path');
// Due to the limitations of realpath(), the source file must exist.
touch('public://foo.txt');
$this->assertTrue($method->invoke($plugin, $public_dir . '/foo.txt', 'public://foo.txt'));
unlink('public://foo.txt');
$temporary_file = '/tmp/foo.txt';
touch($temporary_file);
$this->assertTrue($method->invoke($plugin, $temporary_file, 'temporary://foo.txt'));
unlink($temporary_file);
}
/**
* Tests various invocations of the isLocalUri() method.
*/
public function testIsLocalUri() {
$plugin = $this->destination;
$method = new \ReflectionMethod($plugin, 'isLocalUri');
$method->setAccessible(TRUE);
$this->assertTrue($method->invoke($plugin, 'public://foo.txt'));
$this->assertTrue($method->invoke($plugin, 'public://path/to/foo.txt'));
$this->assertTrue($method->invoke($plugin, 'temporary://foo.txt'));
$this->assertTrue($method->invoke($plugin, 'temporary://path/to/foo.txt'));
$this->assertTrue($method->invoke($plugin, 'foo.txt'));
$this->assertTrue($method->invoke($plugin, '/path/to/files/foo.txt'));
$this->assertTrue($method->invoke($plugin, 'relative/path/to/foo.txt'));
$this->assertFalse($method->invoke($plugin, 'http://www.example.com/foo.txt'));
}
/**
* Do an import using the destination.
*
* @param array $row_values
* An array of row values.
* @param string $destination_path
* The destination path to copy to.
* @param string $source_base_path
* The source base path.
* @return array
* An array of saved entities ids.
*
* @throws \Drupal\migrate\MigrateException
*/
protected function doImport($row_values, $destination_path, $source_base_path = '') {
$row = new Row($row_values, []);
$row->setDestinationProperty('uri', $destination_path);
$this->destination->configuration['source_base_path'] = $source_base_path;
// Importing asserts there are no errors, then we just check the file has
// been copied into place.
return $this->destination->import($row, array());
}
}
class TestEntityFile extends EntityFile {
/**
* This is needed to be passed to $this->save().
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
public $mockEntity;
/**
* Make this public for easy writing during tests.
*
* @var array
*/
public $configuration;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
public $storage;
/**
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
public $streamWrapperManager;
/**
* @var \Drupal\Core\File\FileSystemInterface
*/
public $fileSystem;
public function __construct($configuration) {
$configuration += array(
'source_base_path' => '',
'source_path_property' => 'filepath',
'destination_path_property' => 'uri',
'move' => FALSE,
'urlencode' => FALSE,
);
$this->configuration = $configuration;
// We need a mock entity to be passed to save to prevent strict exceptions.
$this->mockEntity = EntityTest::create();
}
/**
* {@inheritdoc}
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
return $this->mockEntity;
}
/**
* {@inheritdoc}
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = array()) {}
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Tests\MigrateDumpAlterInterface.
*/
namespace Drupal\migrate\Tests;
use Drupal\simpletest\TestBase;
/**
* Allows tests to alter dumps after they've loaded.
*
* @s
* @see \Drupal\migrate_drupal\Tests\d6\MigrateFileTest
*/
interface MigrateDumpAlterInterface {
/**
* Allows tests to alter dumps after they've loaded.
*
* @param \Drupal\simpletest\TestBase $test
* The test that is being run.
*/
public static function migrateDumpAlter(TestBase $test);
}

View file

@ -0,0 +1,167 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Tests\MigrateTestBase.
*/
namespace Drupal\migrate\Tests;
use Drupal\Core\Database\Database;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Row;
use Drupal\simpletest\KernelTestBase;
/**
* Base class for migration tests.
*/
abstract class MigrateTestBase extends KernelTestBase implements MigrateMessageInterface {
/**
* The file path(s) to the dumped database(s) to load into the child site.
*
* @var array
*/
public $databaseDumpFiles = array();
/**
* TRUE to collect messages instead of displaying them.
*
* @var bool
*/
protected $collectMessages = FALSE;
/**
* A two dimensional array of messages.
*
* The first key is the type of message, the second is just numeric. Values
* are the messages.
*
* @var array
*/
protected $migrateMessages;
public static $modules = array('migrate');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$connection_info = Database::getConnectionInfo('default');
foreach ($connection_info as $target => $value) {
$prefix = is_array($value['prefix']) ? $value['prefix']['default'] : $value['prefix'];
// Simpletest uses 7 character prefixes at most so this can't cause
// collisions.
$connection_info[$target]['prefix']['default'] = $prefix . '0';
// Add the original simpletest prefix so SQLite can attach its database.
// @see \Drupal\Core\Database\Driver\sqlite\Connection::init()
$connection_info[$target]['prefix'][$value['prefix']['default']] = $value['prefix']['default'];
}
Database::addConnectionInfo('migrate', 'default', $connection_info['default']);
}
/**
* {@inheritdoc}
*/
protected function tearDown() {
Database::removeConnection('migrate');
parent::tearDown();
}
/**
* Prepare the migration.
*
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The migration object.
* @param array $files
* An array of files.
*/
protected function prepare(MigrationInterface $migration, array $files = array()) {
$this->loadDumps($files);
if ($this instanceof MigrateDumpAlterInterface) {
static::migrateDumpAlter($this);
}
}
/**
* Load Drupal 6 database dumps to be used.
*
* @param array $files
* An array of files.
* @param string $method
* The name of the method in the dump class to use. Defaults to load.
*/
protected function loadDumps($files, $method = 'load') {
// Load the database from the portable PHP dump.
// The files may be gzipped.
foreach ($files as $file) {
if (substr($file, -3) == '.gz') {
$file = "compress.zlib://$file";
require $file;
}
preg_match('/^namespace (.*);$/m', file_get_contents($file), $matches);
$class = $matches[1] . '\\' . basename($file, '.php');
(new $class(Database::getConnection('default', 'migrate')))->$method();
}
}
/**
* Prepare any dependent migrations.
*
* @param array $id_mappings
* A list of id mappings keyed by migration ids. Each id mapping is a list
* of two arrays, the first are source ids and the second are destination
* ids.
*/
protected function prepareMigrations(array $id_mappings) {
/** @var \Drupal\migrate\Entity\MigrationInterface[] $migrations */
$migrations = entity_load_multiple('migration', array_keys($id_mappings));
foreach ($id_mappings as $migration_id => $data) {
$migration = $migrations[$migration_id];
// Mark the dependent migrations as complete.
$migration->setMigrationResult(MigrationInterface::RESULT_COMPLETED);
$id_map = $migration->getIdMap();
$id_map->setMessage($this);
$source_ids = $migration->getSourcePlugin()->getIds();
foreach ($data as $id_mapping) {
$row = new Row(array_combine(array_keys($source_ids), $id_mapping[0]), $source_ids);
$id_map->saveIdMapping($row, $id_mapping[1]);
}
}
}
/**
* {@inheritdoc}
*/
public function display($message, $type = 'status') {
if ($this->collectMessages) {
$this->migrateMessages[$type][] = $message;
}
else {
$this->assert($type == 'status', $message, 'migrate');
}
}
/**
* Start collecting messages and erase previous messages.
*/
public function startCollectingMessages() {
$this->collectMessages = TRUE;
$this->migrateMessages = array();
}
/**
* Stop collecting messages.
*/
public function stopCollectingMessages() {
$this->collectMessages = FALSE;
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Tests\SqlBaseTest
*/
namespace Drupal\migrate\Tests;
use Drupal\migrate\Plugin\migrate\source\TestSqlBase;
use Drupal\Core\Database\Database;
/**
* Test the functionality of SqlBase.
*
* @group migrate
*/
class SqlBaseTest extends MigrateTestBase {
/**
* Test different connection types.
*/
public function testConnectionTypes() {
$sql_base = new TestSqlBase();
// Check the default values.
$this->assertIdentical($sql_base->getDatabase()->getTarget(), 'default');
$this->assertIdentical($sql_base->getDatabase()->getKey(), 'migrate');
$target = 'test_db_target';
$key = 'test_migrate_connection';
$config = array('target' => $target, 'key' => $key);
$sql_base->setConfiguration($config);
Database::addConnectionInfo($key, $target, Database::getConnectionInfo('default')['default']);
// Validate we've injected our custom key and target.
$this->assertIdentical($sql_base->getDatabase()->getTarget(), $target);
$this->assertIdentical($sql_base->getDatabase()->getKey(), $key);
// Now test we can have SqlBase create the connection from an info array.
$sql_base = new TestSqlBase();
$target = 'test_db_target2';
$key = 'test_migrate_connection2';
$database = Database::getConnectionInfo('default')['default'];
$config = array('target' => $target, 'key' => $key, 'database' => $database);
$sql_base->setConfiguration($config);
// Call getDatabase() to get the connection defined.
$sql_base->getDatabase();
// Validate the connection has been created with the right values.
$this->assertIdentical(Database::getConnectionInfo($key)[$target], $database);
}
}
namespace Drupal\migrate\Plugin\migrate\source;
/**
* A dummy source to help with testing SqlBase.
*
* @package Drupal\migrate\Plugin\migrate\source
*/
class TestSqlBase extends SqlBase {
/**
* Override the constructor so we can create one easily.
*/
public function __construct() {}
/**
* Get the database without caching it.
*/
public function getDatabase() {
$this->database = NULL;
return parent::getDatabase();
}
/**
* Allow us to set the configuration from a test.
*
* @param array $config
* The config array.
*/
public function setConfiguration($config) {
$this->configuration = $config;
}
/**
* {@inheritdoc}
*/
public function getIds() {}
/**
* {@inheritdoc}
*/
public function fields() {}
/**
* {@inheritdoc}
*/
public function query() {}
}